Start with the problem you are solving, not a feature list. Which people will use the system, how often, and how is the job done today? A vendor who knows what you are trying to achieve often proposes an alternative that costs less; a team that receives only a feature list can only price the list as written.
Set out the scope as short scenarios: who does what, monolith or microservices and what happens next. Just as important, write down what the first release deliberately excludes. A written out-of-scope list removes more disagreement during acceptance than almost anything else in the document. Indicate as well which decisions are settled and which may still change — honest teams price those differently, and concealing the open questions helps no one.
List the constraints. This means existing systems the software has to talk to, existing databases and their quality, security and compliance rules, user volumes, which devices matter and any technology you are committed to. If there is a hard date, say why: an experienced team will often rearrange the plan to protect it, provided they hear about it early.
Say what the word done means for each item. Acceptance criteria do not require formal language: a short paragraph setting out the expected behaviour is sufficient. That one addition shortens the review at the end considerably and eliminates most late-stage disagreement.
One last thing, say what you expect back. Require an itemised estimate, the assumptions used, whatever the team considers risky and web application framework comparison a low number and a high number. Treat a wide range as information, not evasion: it normally identifies where your description is thin. From there tighten that section and ask for a new estimate — the second estimate tends to be much more reliable.