Building apps

Databases and saved queries

Reading your warehouse through saved queries that an admin publishes, and when direct SQL is the right call.

Prefer saved queries

A saved query is a named, versioned SQL template that an org admin publishes against one data connection. Apps and members invoke it by name with typed, named params, so the app never embeds SQL.

await savedQueries();                        // [{ name, description, params }]
await query("revenue_by_month", { year: 2026 });

Manifest: saved_queries: [revenue_by_month].

Templates use :name placeholders that match declared params. Each param is typed string, int, float, or bool. A param declared with a default is optional at invoke time.

Context binds

Templates may reference :_ctx_user_id, :_ctx_user_email, and :_ctx_org. The server injects them from whoever invokes the query. A caller-supplied _ctx* param is rejected with a 400.

where rep_email = :_ctx_user_email

That gives you per-caller row scoping that the caller cannot forge. Use it before you write filtering logic in the backend function.

Direct SQL

Only when someone explicitly asks for it. Always bind parameters:

await data("warehouse").runSQL("select * from orders where id = $1", [id]);
await postgres("warehouse").runSQL(...);     // dialect-pinned variants
await bigquery("analytics").runSQL(...);
await turso("edge").runSQL(...);

Manifest: adhoc_sql: [warehouse].

adhoc_sql grants raw SQL authority and is intentionally scarce. Do not declare it unless someone asked for direct SQL. A saved query is almost always the better answer.

Read-only access is enforced with read-only sessions on Postgres and Turso. BigQuery has no session-level read-only mode, so the privileges of its connection credential are the boundary.

Discovering what exists

Before you design around an integration, look at what the org already has. These commands are org-scoped: they work right after railcode login, with no app and no railcode.json.

railcode db list                    # data connections: name + engine
railcode query list                 # saved queries: name, params, version, description
railcode connector list             # connectors you can reach

query list returns signatures only, never SQL text.

An empty local result does not prove that a source is unsupported. If you cannot reach the instance, ask an admin what is configured.

Running SQL from the terminal

railcode db query "select 1"
railcode db query "select * from orders where total > $1" --params '[100]'
railcode db query --file report.sql --connection analytics

railcode query run my_orders --params '{"region":"emea","limit":5}'

Note the two different --params shapes. db query takes a positional array ('[100]' binds $1). query run takes a named object, exactly matching the SDK call.

Reading the org directory

await appUsers();          // [{ id, email, name, is_admin }]; id matches ctx.user.id
await dataConnectors();    // [{ name, engine }], never a DSN

Both are read-only discovery, for building pickers and showing names.

Publishing a saved query

Creating, updating, and deleting saved queries is organization administration. See administration.

On this page