Before production
A freshly deployed Ramose server accepts every request, from anyone, with full rights. That is the right default for a laptop and the wrong one for the internet. This checklist is for anyone about to point real users at it. (The Ramose server is the one Cloudflare Worker that serves all your databases; Ramose’s code calls it the peer.)
Close the front door
Section titled “Close the front door”- Set a policy, or a shared token. With neither
RAMOSE_POLICYnorRAMOSE_TOKENset, every caller is an administrator on every database. Use a policy for anything a browser talks to; a sharedRAMOSE_TOKENonly when the sole caller is your own backend. - Complete the verifier. A policy needs
RAMOSE_JWKS_URL,RAMOSE_JWT_ISS, andRAMOSE_JWT_AUD. Pass the sameauthobject toRamose.Serverand the deploy fails loudly when one is missing — better than a server that denies every request at runtime. - Narrow the origins. Without a policy the server answers CORS for
*and ignoresRAMOSE_ALLOWED_ORIGINS. Once a policy is set, list your own origins there. An empty list sends no CORS header at all, which blocks browsers entirely — a valid choice for a server only Workers call. - Cap token lifetime.
RAMOSE_JWT_MAX_TTLdefaults to 900 seconds. A revoked grant takes effect on the next write, but a revoked token is only gone when it expires. - Declare
admindeliberately. A token with classadminbypasses every rule and is the only class that may callexplainor the/admin/*routes. Do not hand it to a browser. - Check the demo console is gone. The server serves a small demo app at
/until a policy is configured. Load your server’s root URL and confirm you get a 404. - Pin the internal secret. Set
RAMOSE_INTERNAL_SECRETso the secret the server uses to talk to the writer — the one thing per database that commits writes (glossary) — does not change on every deploy. - Pass your pull patterns to the compiler.
Ramose.Policy.compile(policy, { pulls: [...] })turns “this masked field silently deletes rows” into a build error. Withoutpulls, the check does not run.
Decide what you keep
Section titled “Decide what you keep”- Know how history is kept. History is kept as version numbers, not
dates:
asOftakes at. Nothing prunes history —RAMOSE_RETAIN_ROOTSbounds how much storage the index costs, not how far backasOfreads — see Retention. - Have a copy story. Ramose ships no export command. Your data lives in your own R2 bucket plus the writer’s storage, so a bucket-level copy captures everything already folded into a snapshot but not writes newer than the last one.
- Know what a teardown deletes. Deleting a
Ramose.DatabaseorRamose.Serverresource deletes no data, on purpose. The rest of the stack is another matter:
Size it honestly
Section titled “Size it honestly”- Know the write ceiling. In our benchmark (
bench/RESULTS.mdin the repo) one database sustained a few hundred writes per second on Cloudflare — 166–879 per second depending on client count. Past that you split across databases: a function call, not a deployment. There is exactly one writer per database and that is not configurable. - Split rather than scale up. More write throughput means more databases, divided along ownership lines. There are no joins across databases: a view that spans them is one query per database, merged in your app (The write ceiling).
- Set a query budget you understand.
RAMOSE_QUERY_MAX_CELLSdefaults to 1,572,864 cells (about 48 MB of intermediate results). Over-budget queries fail withQueryBudgetExceeded(413), and a live query does not retry them. -
limitbounds what you receive, not what the query scans. Thewhereclauses still build the full intermediate result, so a broad query withlimit(20)can still exceed the budget. Narrow withwhere, not withlimit.
Handle the failures that will happen
Section titled “Handle the failures that will happen”- 503
Unavailable. The writer restarts (deploys, storage faults) and returns aretryAfterMs. Retrying is safe: nothing from a failed batch is kept, and version numbers never gap. Surface it as a retry, not an error. - 413
QueryBudgetExceeded. Not retryable. Log the clause the error names and fix the query. - 403 / 409 on writes. A policy refusal arrives as
Unauthorizedfrom the server’s first check orTxRejectedfrom the writer. Handle both — see Permissions. - Reconnection is already handled. Live queries retry network
failures with backoff and re-authenticate in place. Four failures are
terminal and need your attention:
InvalidRequest,DatabaseNotFound,Unauthorized,QueryBudgetExceeded.
Deploy hygiene
Section titled “Deploy hygiene”- Use stages.
bun alchemy deployships your personal stage;--stage prodis production. Stages are isolated copies of the whole stack. - Expect the first deploy to wait.
Ramose.ServerprobesGET /healthup to 30 times, 2 seconds apart. Slow first DNS looks like a hang;probe: falseskips it. - Install the schema from one place.
Ramose.Databaseat deploy for known databases,db.install()at signup for per-customer ones. Never per request.