Railway hosting
Railway hosting is Subspace’s managed offering for people who don’t want to run their own box: a dedicated instance per tenant, run by a fleet operator, without giving up the trust properties that make Subspace worth self-hosting in the first place. It’s the same server, the same embedded Postgres, the same data model, running on infrastructure someone else watches.
Tenant-per-instance, not multi-tenant
Section titled “Tenant-per-instance, not multi-tenant”Each tenant gets its own tenant cell: one Railway service, one volume-backed instance, running the same production image you’d run yourself. There’s no shared database, no shared process, no row-level tenant isolation to get subtly wrong. A tenant’s data lives in a Postgres cluster only that tenant’s cell process can reach, the same isolation a self-hosted box gives you, provisioned on demand.
Trust boundaries
Section titled “Trust boundaries”The managed design has five credential and failure domains. Arrows show permitted calls, not shared trust:
operator ── fleet credential ──> control plane ── provisioning API ──> Railway │ └── per-cell escrow API ◀── tenant cell │ provider OAuth/API calls ◀─────────────────┤ ├── encrypted WAL/blobs ──> backup storage │browser/mobile ── tenant auth ──────────────────────────────────────┤ │paired device ◀── signed jobs, approval, resumable output ──────────┘- The control plane holds the Railway fleet credential and its own database credential. Neither is injected into a tenant service. It may mint a distinct, revocable escrow credential for that one cell.
- A tenant cell holds only tenant-scoped provider credentials, backup-prefix credentials, and its per-cell escrow credential. It never receives a Railway API token, the control-plane database URL, or another cell’s credential.
- A paired device owns local execution and consent. It receives signed tenant jobs, not infrastructure credentials. The cell cannot turn a device grant into access to another tenant.
- Providers receive only the credential for the bound tenant/account. Provider responses remain untrusted input at the connector boundary.
- Backup storage accepts tenant-prefixed encrypted WAL and blobs. It has no route back into a running cell or the control plane.
The baseline topology uses a separate Railway project/environment per tenant and a
separate project for fleet services. Railway private DNS is flat inside its scope, so
cells must not share that scope until the platform supplies an independently verified
isolation control. Generic agent egress (web.fetch and custom-function HTTP methods)
also rejects localhost, IP literals, .local, and *.railway.internal, validating
every redirect hop rather than delegating redirects to the HTTP client.
The control plane
Section titled “The control plane”The control plane ships alongside the fleet, because operating it has to be cheap:
- Tenant registry: one row per tenant, tracking the Railway service, the volume, the domain, the plan, and the deployed version.
- Idempotent provisioning: a new tenant is created through Railway’s API from a
template, a fresh service, a fresh volume,
SUBSPACE_PROFILE=cloud, a claim token, and a KMS-wrapped key reference. A Railway-generated domain is available immediately; a customslug.subspace.appsubdomain attaches asynchronously (certificate issuance is not instant). Provisioning is zero-touch: request a tenant, get a claim link. - Fleet operations: staged upgrades roll out to a canary tenant first, then
cohorts, gated on workflow replay-compatibility checks so an in-flight agent run
never gets stranded mid-upgrade by a bundle change. A Postgres major-version bump
rides the same
pg_upgradeorchestration a self-hosted instance uses, forced through a pre-upgrade backup first. - Monitoring: every cell exposes a status endpoint (version, migration state, backup freshness, disk usage, queue depth), polled by the control plane so a silently-stopped backup job is caught by monitoring, not by needing a restore.
The initial edge decision is deliberately simple: issue the Railway-generated domain first and attach a custom domain asynchronously. A shared wildcard router is deferred until measured fleet traffic justifies another public component and Railway confirms the wildcard/service semantics in writing. A wildcard certificate or DNS record is not treated as a dynamic tenant router.
Platform-validation gate
Section titled “Platform-validation gate”This architecture is not commercially approved merely because one container boots. Before a ten-tenant design-partner fleet, retain one evidence packet containing:
- Written Railway answers for project, service, and volume limits and increase terms; API rate limits; wildcard attachment/routing semantics; and the supported isolation boundary for private networking.
- Five to ten representative volume-backed cells, each running realistic sync and
agent load. Record image bytes, cold-start-to-
/readyz, steady and peak RSS/CPU, volume I/O, deploy unready interval, backup bytes/elapsed time, and a timed restore to a fresh volume. - Per-cell and shared monthly spend from the actual experiment. Pricing uses the measured floor and observed variance, never an estimate copied from a calculator.
- A restore result proving the selected recovery point, referenced blobs, and an in-flight durable workflow; plus a verified export imported by the self-hosted image.
- A decision record naming the domain strategy and tenant isolation unit, with the capacity path beyond the platform’s default volume allowance.
Use the same commit and load fixture for every cell, synchronize clocks, retain raw platform metrics and logs, and report p50/p95/max rather than a single favorable run. Any missing written limit, cross-cell reachability, unscoped credential, failed restore, or unexplained cost outlier fails the gate.
Provider fallback
Section titled “Provider fallback”The cell and control-plane contracts are provider-neutral. The same OCI image mounts
SUBSPACE_HOME on a Hetzner host or Fly volume, exposes the same health endpoints, and
uses the same backup, escrow, device-job, and export protocols. A provider move replaces
only the provisioning/domain/metrics adapter and its fleet credential; it does not
change tenant data, agent workflows, or the device execution protocol. Validate that
fallback with the same evidence packet before relying on it as an operational escape
hatch.
The device execution lane
Section titled “The device execution lane”Terminals, coding agents, os.exec, Jupyter kernels, and macOS automation don’t run
on the tenant cell in the cloud. They run on your paired device, the same laptop
or desktop you’d run Subspace on yourself, with the cell acting as the durable
coordinator. This is what makes hosted execution safe: a cloud process holding your
shell is a much larger blast radius than a cloud process holding your notes, so the
execution capability stays on the device you already trust, and the cell only ever
dispatches jobs to it.
- A durable job (queued, offered, awaiting device approval, running, and a terminal
state including
succeeded,failed,cancelled,expired, orunknown) is the unit of work between the cell and your device.unknownmatters as much as the success states: if a device’s acknowledgment of a non-idempotent command is lost, the job goes tounknownrather than being silently retried, so a command that might have already run never runs twice by accident. - Every dispatch passes a seven-point authorization check: the run’s tenant matches the target tenant, the run’s owner is an active member, the target device belongs to that same owner (an admin’s elevated status does not satisfy this), the device has advertised the capability the job needs, the agent definition grants that capability, cloud policy allows it, and your device-local consent policy (ask every time, allow a signed action, or deny while locked) allows it too.
- Terminals and coding agents keep running on your device even if the socket to the cell drops, including Railway’s periodic WebSocket rotation described below; the local process survives the disconnect and a reconnect resumes streaming from where it left off, rather than the job dying with the socket.
- Server-host execution (running these same capabilities on the tenant cell itself) stays available only in local, self-hosted mode. On a hosted tenant it isn’t an option, by design, not by policy toggle.
Coding agent dispatch already distinguishes where code actually executes; hosted Subspace pushes that distinction through the whole product.
Storage durability
Section titled “Storage durability”A tenant cell’s data lives on its Railway volume at $SUBSPACE_HOME, exactly the
home directory layout a self-hosted
instance uses. On top of that:
- Per-tenant encryption key escrow: each tenant has its own data-encryption key.
The cell wraps it with AWS KMS under a tenant-bound encryption context, then sends only
the envelope to the authenticated control-plane escrow API. The control plane never sees
the plaintext key, and its database is separate from the tenant cell. The same image runs
the deployable API when a dedicated Railway service sets
SUBSPACE_SERVICE=key-escrow-control-plane, attaches external PostgreSQL throughDATABASE_URL, and provisions cell credentials through the platform secret manager. The adapter serializes generation changes per tenant and commits each metadata-only audit row in the same transaction as the envelope operation. - WAL-G and blob backups: the same continuous WAL archiving and blob replication described in backup and restore run per tenant, writing to a per-tenant storage prefix.
The escrow service exposes /livez and database/migration-aware /readyz probes. Point
Railway readiness at /readyz; SIGTERM immediately removes the instance from readiness,
rejects new escrow work, drains accepted requests, and then closes the database pool. The
control-plane database must be backed up independently of tenant-cell volumes because its
wrapped generations are what make a lost-volume recovery possible.
Public-exposure hardening
Section titled “Public-exposure hardening”A tenant cell is, unlike a self-hosted instance, reachable from the public internet by
design, so it runs under a dedicated cloud capability profile
(SUBSPACE_PROFILE=cloud) that closes the gaps a loopback-only deployment doesn’t
need to think about: claim-token provisioning
is mandatory rather than optional, internal broker and workflow endpoints bind on a
separate loopback-only listener that never rides the public port, and the profile
disables every capability that only makes sense with a trusted local operator at the
keyboard, server-host os.exec, container execution, server-host PTY spawn, Jupyter
kernel spawn, and local MLOps launchers, in favor of the device execution lane above.
Client pairing and reconnect
Section titled “Client pairing and reconnect”All four clients, web, desktop, mobile, and the browser extension, pair against a hosted, claimed tenant the same way they pair against a self-hosted instance. Railway recycles public WebSocket connections roughly every 15 minutes and doesn’t offer sticky sessions. Every satellite socket—the desktop PTY/control relay, extension, mobile bus, browser terminal, and both meeting-audio channels—uses a one-use socket ticket and rotates with jitter between 12 and 14 minutes. Topic sockets resume from their last durable outbox cursor; PTY sources resume from a desktop-owned output cursor and authoritative scrollback snapshot; audio buffers while the transport is being replaced. A relay close is not a PTY or meeting lifecycle event, so a forced reconnect is not visible as lost terminal output or a dropped job.
Google OAuth restricted-scope verification
Section titled “Google OAuth restricted-scope verification”Gmail and Google Calendar connectors need Google’s restricted-scope verification, a security assessment separate from ordinary OAuth app review, before mainstream users can connect their accounts on a hosted tenant. Because a hosted deployment serves a public origin (unlike a self-hosted instance, where each operator registers their own OAuth credentials), the verification and the redirect-URI design are centralized: Google doesn’t allow wildcard redirect URIs, so the control plane runs one registered OAuth callback and relays the result to the correct tenant rather than each tenant subdomain registering its own.
Billing, quotas, and compliance
Section titled “Billing, quotas, and compliance”Hosted tenants run under plan-based quota enforcement wired to the control plane (suspension and hard caps on overage), with LLM usage metered at the same broker seam that already mediates every model call, so usage-based limits don’t require a second accounting path. Documented recovery-point and recovery-time objectives, published data-retention and deletion policies, and supply-chain practices (image signing, SBOM, vulnerability scanning) round out the managed service.