Backend runtime contract
What your app's backend can rely on at runtime: the database role, transactions, timeouts and size limits, each marked supported, current behavior or not provided.
This page lists what your app's backend endpoints, background tasks and scheduled jobs can rely on: how they connect to the database, what a transaction means, which timeouts exist and the size limits. Anything not on this page is not part of the contract.
- Supported
- You can build on it. Floot keeps it, and a change is announced in the changelog before it ships.
- Current behavior
- True today, but it may change without notice. Fine for performance, but your code should not depend on it to be correct.
- Not provided
- Floot does not offer it. The details say what to do instead.
Database connection
| Item | Status | Details |
|---|---|---|
| Connection string | Supported | Your backend reads it from FLOOT_DATABASE_URL. It is a direct connection to Postgres with no connection pooler in between, and the preview and the published app use the same database. |
| Database role | Supported | The app connects as the database's owner role (neondb_owner for databases Floot creates). That role owns the tables your app creates, so it can read, write and change all of them. Run select current_user to see it. |
| A separate, restricted login for the app | Not provided | There is one role for everything: your app, the Data tab and your assistant's SQL tools. |
| Row-level security | Not provided | Policies don't apply to a table's owner, which is the role the app uses. Whether the role also has BYPASSRLS depends on when the database was created: select rolbypassrls from pg_roles where rolname = current_user tells you. Check who may see a row in your endpoint code, not with policies. |
| Session state across requests | Not provided | Connections are opened when a request needs them and closed when it ends. A plain SET, a temporary table, a prepared statement or a session-level advisory lock doesn't carry over to the next request. |
| Reconnecting | Current behavior | When the database is waking up or a connection drops, Floot's database client opens a new connection and tries again, up to 4 attempts with a 30-second connect timeout each. A query outside a transaction that failed because the connection was lost can be sent again. Statements inside a transaction are never sent again (see below). |
Transactions
| Item | Status | Details |
|---|---|---|
| No outer transaction | Supported | Floot never wraps a request, task or scheduled job in a transaction. Your own BEGIN is the top-level transaction, and a statement outside one commits on its own as soon as it succeeds. |
| A transaction that resolves | Supported | A transaction runs on one connection from start to finish. await db.transaction().execute(fn) resolves only after Postgres has answered your COMMIT. When your callback returns, nothing is committed yet; the promise is what counts. |
| No replay | Supported | Floot doesn't run your transaction callback again or send COMMIT a second time. The only retry is opening the transaction's connection, before your callback has started. Floot never sends an HTTP request to your backend a second time either. |
A connection lost during COMMIT | Not provided | Your code gets an error and Floot can't tell you whether the commit went through. For writes that must happen exactly once, give each one a unique key, so you can check afterwards and a second attempt is refused. |
| Background task retries | Current behavior | A task or job that throws, or that was cut off at its time limit, is not started again by Floot. The exception is a database connection failure before any of the task's work reached the database or the network. |
Database timeouts
Floot sets no statement_timeout or lock_timeout on your database, so a query can wait for a lock or run until the request's own time limit. To bound them, set them yourself as the first statements of the transaction:
BEGIN;
SET LOCAL lock_timeout = '1s';
SET LOCAL statement_timeout = '3s';
-- your queries
COMMIT;- This is supported: Postgres itself enforces both and cancels the statement on the server, and because the connection is direct, they apply to exactly that transaction and end with it.
- With Kysely, run both through Kysely's
sqltag ontrx, as the first thing in yourdb.transaction().execute(async (trx) => …)callback. - A plain
SET(withoutLOCAL) lasts only until the connection closes at the end of the request, so it isn't a reliable way to set a limit. ALTER ROLE … SET statement_timeoutwould apply to everything that uses the role, including the Data tab and your assistant's SQL tools, so Floot doesn't recommend it.
Run time, memory and size limits
| Limit | Status | Value |
|---|---|---|
| Background task or scheduled job | Supported | Up to 15 minutes. A task is stopped about 30 seconds before that, so it can hand off what's left to a new task. |
| Request from a browser or another service | Supported | It must start answering within 2 minutes and never go quiet for longer than that. Otherwise the caller gets a 504 error, even though your code may keep running for up to 15 minutes. For longer work, queue a background task and answer at once. |
| Memory | Supported | 1 GB by default (2 GB with server-side rendering). Ask your assistant to set anything from 512 MB to 4 GB. |
| Request body | Supported | About 6 MB. For bigger uploads, send the file to storage and pass its key to your endpoint. |
| Response body | Current behavior | Responses are streamed, so they can be larger than the request limit: up to 200 MB. |
| Your backend's own outgoing requests | Current behavior | Floot puts no size or time limit on calls your backend makes, other than the memory and run time above. A download read as a stream (for example for await over response.body) is never held in memory whole, so you can hash it piece by piece. |
| Cancelling an outgoing request | Not provided | AbortController / AbortSignal work as they do in Node.js. Floot adds no signal confirming that the other server stopped. When a request or task ends, connections still open are closed. |
Not part of the contract
- A release ID or lockfile for a publish. Each publish installs your dependencies again from the versions in
package.json. - The typecheck environment. The compiler settings and the platform's type declarations are not published. Your assistant's
typechecktool returns the errors it finds, nothing more. - A per-project hard spending cap. The daily hosting limit pauses published apps and is based on an estimate, so a day can end slightly above it.