Skip to content

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.)

  • Set a policy, or a shared token. With neither RAMOSE_POLICY nor RAMOSE_TOKEN set, every caller is an administrator on every database. Use a policy for anything a browser talks to; a shared RAMOSE_TOKEN only when the sole caller is your own backend.
  • Complete the verifier. A policy needs RAMOSE_JWKS_URL, RAMOSE_JWT_ISS, and RAMOSE_JWT_AUD. Pass the same auth object to Ramose.Server and 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 ignores RAMOSE_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_TTL defaults to 900 seconds. A revoked grant takes effect on the next write, but a revoked token is only gone when it expires.
  • Declare admin deliberately. A token with class admin bypasses every rule and is the only class that may call explain or 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_SECRET so 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. Without pulls, the check does not run.
  • Know how history is kept. History is kept as version numbers, not dates: asOf takes a t. Nothing prunes history — RAMOSE_RETAIN_ROOTS bounds how much storage the index costs, not how far back asOf reads — 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.Database or Ramose.Server resource deletes no data, on purpose. The rest of the stack is another matter:
  • Know the write ceiling. In our benchmark (bench/RESULTS.md in 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_CELLS defaults to 1,572,864 cells (about 48 MB of intermediate results). Over-budget queries fail with QueryBudgetExceeded (413), and a live query does not retry them.
  • limit bounds what you receive, not what the query scans. The where clauses still build the full intermediate result, so a broad query with limit(20) can still exceed the budget. Narrow with where, not with limit.
  • 503 Unavailable. The writer restarts (deploys, storage faults) and returns a retryAfterMs. 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 Unauthorized from the server’s first check or TxRejected from 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.
  • Use stages. bun alchemy deploy ships your personal stage; --stage prod is production. Stages are isolated copies of the whole stack.
  • Expect the first deploy to wait. Ramose.Server probes GET /health up to 30 times, 2 seconds apart. Slow first DNS looks like a hang; probe: false skips it.
  • Install the schema from one place. Ramose.Database at deploy for known databases, db.install() at signup for per-customer ones. Never per request.