Organization administration
Members, roles and grants, data connections, saved queries, service connectors, analytics, and observability logs.
Management commands are org-scoped. They work from any directory after railcode login and
need no app or railcode.json. Most references accept a name, slug, or email, or a UUID.
Owner and admin permissions are enforced on the server. A 403 marks an authority boundary.
Do not work around it with another credential.
Work in this order: inspect with the matching read command, apply the narrowest mutation, then verify with the read command again.
Members
railcode members list
railcode members set-role <email|uuid> --role admin|member
railcode members remove <email|uuid>Any member may list members. Mutations require admin authority. The owner tier is not
assignable through set-role.
There is no CLI command that creates a member. A new person joins through the invite
flow in the web app, and set-role then adjusts their tier.
Custom roles and grants
railcode roles list
railcode roles create --name <name> [--description <text>]
railcode roles update <role> [--name <name>] [--description <text>]
railcode roles delete <role>
railcode roles add-member <role> <email|uuid>
railcode roles remove-member <role> <email|uuid>
railcode roles grants
railcode roles grant --subject <org|role:X|user:X> --resource <type> --ids <a,b>
railcode roles revoke <grant_id>
railcode roles materialize --subject <org|role:X|user:X> --resource <type>
railcode roles effective <email|uuid>
railcode roles catalogGrant resource types: app, llm, email, sc_endpoint, connector, saved_query, and
agent.
Grants are additive. --ids is a comma-separated list and may use *, but prefer explicit
ids. materialize expands wildcard access into the resources that exist now, so future
resources are not included automatically. Inspect the subject and resource first, because it
changes how grants are maintained later.
Use catalog to discover grantable names and effective to verify a member's computed
access.
App authority declared in manifest.yaml is ratified against this same grant model. This is
why a deploy can be blocked for adding authority the deployer does not hold.
Saved queries
railcode query list
railcode query run my_orders --params '{"region":"emea","limit":5}'
railcode query create --name my_orders --connection analytics \
--sql "select * from orders where region = :region limit :limit" \
--params '[{"name":"region","type":"string"},{"name":"limit","type":"int","default":20}]'
railcode query update my_orders --sql-file my_orders.sql
railcode query delete my_ordersCreate, update, and delete are admin-only. list and run are member operations, and
invocation can be grant-gated per query.
Templates use :name placeholders with declared types string, int, float, or bool.
run --params is a JSON object. Create/update --params is an array of parameter
declarations. SQL comes from --sql or --sql-file, never both.
update uses PATCH semantics: omitted fields stay unchanged, and --clear-params declares
none. SQL and param edits bump the version while preserving grants. Delete removes the saved
query and any grants that name it.
There are no identity binds. :_ctx_user_id, :_ctx_user_email, and :_ctx_org were
removed, and a placeholder or param named _ctx* is rejected at create and at run time with
_ctx binds are no longer supported. The caller's identity decides the grant check and the
audit log, never the SQL. For per-caller rows, declare a typed param and have the caller pass
the value. list exposes signatures, never SQL text.
Data connections
railcode connections list
railcode connections create --name <n> --kind postgres \
--config-file connection.json --credentials-file credentials.json
railcode connections delete <name|uuid>Create replaces a same-name connection and connects to it before saving.
| Kind | Config | Credentials |
|---|---|---|
postgres | {host, database, username, port?, sslmode?, ssh?} | {password} |
bigquery | {project_id, dataset} | {service_account_json} |
turso | {url} | {auth_token} |
Inline --config and --credentials are supported, but the file options keep credentials
out of shell history.
Deleting a connection can break saved queries and apps. Inspect dependents first.
If the database sits behind a firewall, allow our IP address first.
Postgres through an SSH bastion
If your Postgres database is on a private network, Railcode can reach it through an SSH bastion. We connect to the bastion, and the bastion forwards the connection to the database. Apps, agents, and saved queries use the connection name as usual. Nothing changes for them.
You need:
- A bastion with a public address that accepts SSH from our IP address.
- A user on the bastion that can forward TCP to the database. Make a new SSH key pair for
Railcode, put the public key in that user's
authorized_keys, and give us the private key. We only use public-key authentication. - The bastion's host public key. This identifies the bastion itself. It is not the user's
key. Ask the bastion admin for the contents of
/etc/ssh/ssh_host_ed25519_key.pub. Enter thessh-ed25519 AAAA…key, not a fingerprint and not a wholeknown_hostsline.
In the dashboard, open Data sources, click Add data source, and choose Postgres.
Enter the database host and port as the bastion sees them. A private IP, a private DNS name,
or localhost all work. Then tick Connect through an SSH bastion and fill in the bastion
host, SSH port, SSH username, trusted host public key, and SSH private key. Add the passphrase
if the key has one.
With the CLI (0.3.7 or later), add ssh to the config file:
{
"host": "postgres.internal.example",
"port": 5432,
"database": "warehouse",
"username": "reader",
"sslmode": "require",
"ssh": {
"host": "bastion.example.com",
"port": 22,
"username": "railcode",
"host_key": "ssh-ed25519 AAAA..."
}
}Put the database password in credentials.json, plus ssh_passphrase if the key is
encrypted. Pass the private key as a separate file:
railcode connections create --name warehouse --kind postgres \
--config-file connection.json --credentials-file credentials.json \
--ssh-key-file ./railcode-bastion.pemThe private key and passphrase are stored encrypted with the database password. They never go
in the config. The connection list marks these connections (SSH tunnel). To switch back to
a direct connection, create the same name again without ssh.
Postgres TLS stays on through the tunnel. verify-full checks the database certificate against
public certificate authorities. For a database with a self-signed certificate, use require.
If the host key changes, we refuse to connect until you update the connection. Every query opens a new SSH connection, so queries take a little longer than on a direct connection. One bastion per connection is supported.
When you create the connection, we test it before saving. Errors tell you what to fix:
| Error | What to check |
|---|---|
| Host key does not match | The trusted host public key. Get it again from the bastion. |
| SSH authentication failed | The SSH username, and that the key pair's public key is in its authorized_keys. |
| SSH connection timed out | That the bastion is reachable: its address, port, and firewall. |
| Bastion does not allow forwarding | AllowTcpForwarding and PermitOpen in the bastion's sshd_config. |
| Bastion could not reach the database | The database host and port, as the bastion sees them. |
| Postgres could not connect | The database name, user, password, and SSL mode. |
| Host is not allowed | The bastion must have a public address. |
Service connectors
railcode connector list --admin # base URL, credential state, enabled state, native key
railcode connector native # presets and their credential fields
railcode connector enable <key> --credentials-file credentials.json
railcode connector create --name <n> --base-url <url> --auth-type bearer \
--auth-config-file auth.json --methods GET,POST --description <text>
railcode connector delete <name|uuid>enable creates or rotates a native connector credential. Use --disabled to create one
disabled, and restrict --methods to the verbs required.
The member-facing connector list, docs, and fetch are invocation tools. fetch can
cause downstream side effects, so use it as a deliberate verification call.
If the API only accepts known addresses, allow our IP address.
Analytics
railcode analytics <app> [--range 7d|30d|90d]Default range 30d. Output includes totals, uniques, daily values, top paths, and top
users.
Observability logs
railcode logs <stream> [filters]
railcode logs <stream> <request_id> # full JSON detail| Stream | What it covers | Capability |
|---|---|---|
connector | Data-connection SQL | connection:manage |
service-connector | Proxied HTTP calls | service-connector:manage |
llm | LLM gateway calls | llm:manage |
email | Email gateway sends | email:manage |
agent | Managed-agent runs | agent:manage |
Filters are validated per stream: --app, --user, --connector, --agent,
--source app|agent (LLM only), --status, and --limit <1..500> (default 100).
Per-app backend logs (railcode logs app) are a different surface. They are available to
anyone with edit rights on the app, rather than admin-only. See
deploying.
Working principles
- Grant specific resources instead of
*unless broad access is explicitly intended. - Prefer saved queries over ad-hoc SQL authority.
- Restrict service-connector HTTP methods to what is needed.
- Use the file options for credentials rather than inline flags.
- Propose archiving over deleting when the goal is to retire an app. Archive is inert and reversible; delete is not.
app kv/app filesmutate live tenant data, including individual members' private scopes. Inspect first. Mutate only what was asked for.