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 catalog

Grant 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_orders

Create, 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.

KindConfigCredentials
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 the ssh-ed25519 AAAA… key, not a fingerprint and not a whole known_hosts line.

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

The 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:

ErrorWhat to check
Host key does not matchThe trusted host public key. Get it again from the bastion.
SSH authentication failedThe SSH username, and that the key pair's public key is in its authorized_keys.
SSH connection timed outThat the bastion is reachable: its address, port, and firewall.
Bastion does not allow forwardingAllowTcpForwarding and PermitOpen in the bastion's sshd_config.
Bastion could not reach the databaseThe database host and port, as the bastion sees them.
Postgres could not connectThe database name, user, password, and SSL mode.
Host is not allowedThe 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
StreamWhat it coversCapability
connectorData-connection SQLconnection:manage
service-connectorProxied HTTP callsservice-connector:manage
llmLLM gateway callsllm:manage
emailEmail gateway sendsemail:manage
agentManaged-agent runsagent: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 files mutate live tenant data, including individual members' private scopes. Inspect first. Mutate only what was asked for.

On this page