23blocks in a Box
The engineering companion to graduating from shared services to dedicated infrastructure.
This is the how a customer actually does it reference: the portability contract every block honors, the six-phase offboarding process, the reversible cutover playbook, and the hard-won lessons that are now baked into the tooling.
If you're evaluating whether — and how — to move your tenant off shared 23blocks and onto infrastructure you own, this document is for your engineering team.
What "graduation" means, technically
A customer starts on the shared, multi-tenant 23blocks platform, then outgrows it. 23blocks in a Box is the path that takes the exact same platform — the same open-source-forked blocks, the same APIs, the same data — into the customer's own cloud account, with no code rewrite and a clean rollback.
The technical promise rests on one property: every block is fully configured by environment variables. There are no hardcoded hostnames and no baked-in secrets. The same container image runs on shared 23blocks or in any customer account, unchanged. Graduation is therefore a matter of where the containers run and what URLs the apps point at — not a matter of different code.
The portability contract
Every block follows the same environment-variable contract. Because configuration lives entirely in the environment, the same image is portable across any runtime — a managed container platform, Docker Compose, Kubernetes, or a bare docker run.
Required variables
Every block must have these set, or the container refuses to start:
| Variable | Description |
|---|---|
DB_HOST | PostgreSQL hostname |
DB_NAME | Database name |
DB_USERNAME | Database user |
DB_PASSWORD | Database password |
SECRET_KEY_BASE | Rails secret key (64+ hex chars) |
REDIS_URL | Redis URL with DB number, e.g. redis://host:6379/0 |
ENCRYPTION_SERVICE_KEY | Per-tenant encryption master key (fail-fast) |
ENCRYPTION_SERVICE_SALT | Encryption salt (fail-fast) |
Also required — inter-service configuration. Beyond the table above, each block fail-fasts on its own service configuration — for example
AUTH_API_URL,AUTH_ISSUER,SELF_API_URL,API_KEY,S3_BUCKET_NAMESPACE, and per-block extras (SEARCH_API_URL;JARVIS_API_URL/WS_HOSTfor the realtime block, and so on). A missing one prevents the container from booting — this is exactly how a first boot fails on new infrastructure. SeeENV-VARS.mdfor the complete per-block set.
Common optional variables
| Variable | Default | Description |
|---|---|---|
RAILS_ENV | production | Rails environment |
DB_PORT | 5432 | PostgreSQL port |
PORT | 80 | HTTP listen port (Puma) |
SENTRY_DSN | (none) | Error tracking; omit to disable |
RAILS_MAX_THREADS | 5 | Puma thread pool size |
WEB_CONCURRENCY | 2 | Puma worker count |
RAILS_LOG_TO_STDOUT | true | Send logs to STDOUT for container logging |
For runtime deployment patterns — injecting these variables via Docker Compose, Kubernetes/Helm, or Terraform — see the Self-managed deployment guide.
Health checks
Every block exposes the same health endpoints. Use GET /health/ready for load-balancer health checks and readiness probes.
| Endpoint | Purpose |
|---|---|
GET /health | Basic liveness check |
GET /health/ready | Readiness (DB + Redis connected) |
GET /health/detailed | Full status with version info |
Why this matters for graduation: because the contract is identical on shared and dedicated, "moving" a block is really just starting the same image in a new place with new values for DB_HOST, REDIS_URL, and friends. Nothing about the block changes.
The offboarding process (six phases)
Graduation runs as six phases. The first four can happen well ahead of time, without any user impact; the last two happen in the cutover window.
1. Fork
Each block is open source, published to a public upstream. The customer forks each block into their own org. No CI/CD configuration or secrets travel to the open-source copy. From this point, the customer owns the code and can git pull upstream to take future updates on their own schedule.
2. Build
The fork's own CI pipeline builds the container images and pushes them to the customer's own image registry. The customer's build produces the customer's artifacts.
3. Provision
Stand up the dedicated, single-tenant footprint in the customer's account — the same architecture that runs 23blocks, sized for one tenant. Networking, the load balancer, the container services, a PostgreSQL instance with a database per block, a Redis cluster, object storage, and TLS certificates.
4. Migrate data
Copy the tenant's data from shared 23blocks into the customer's database:
- Per-block tenant schemas are copied whole.
- Control rows are filtered to the tenant.
- Postgres sequences are advanced to
MAX(id)after the copy (see the lessons below — this is not optional). - Row parity is verified table by table.
5. Cut over the consumers
Flip each application's configuration from the shared-cloud hosts to the customer's own hosts. This is a URL change, nothing else — the same API key is already valid on both environments.
6. Verify
Prove two things: that there is zero traffic left on shared services from this tenant, and that the product works end-to-end on the new infrastructure.
The cutover playbook (the graduation window)
The final flip runs inside a single, reversible maintenance window.
Maintenance window → static source
All of the tenant's writers go quiet so the source data is frozen while the final copy runs:
- SPAs show a maintenance page.
- Backends are brought process-down.
- Very-low-write surfaces may take an accepted no-freeze.
Final delta re-sync
Bring the customer's database to an exact, current copy of the shared tenant. Reset sequences. Verify parity per table.
URL-only consumer flip
The same API key is already valid on both environments, so cutover is purely a base-URL change:
*.api.us.23blocks.com → *.api.us.<customer-domain>
The only difference between "on shared" and "on your own infra" is the URL.
Version = which infrastructure
Each app cuts a new major version whose only change is the URLs.
- New major → own infrastructure.
- Previous major → shared.
The running version number alone tells you where an app points, and rollback is simply redeploying the previous major.
Verify behind the maintenance page
Before any user is let back in, check — behind the maintenance page:
- Auth — sign in, then validate the token.
- The full read path.
- One real write.
Any issue is caught with zero user exposure.
Rollback = flip URLs back
Rollback is instant and needs no rebuild: flip the URLs back to shared. Because the shared-cloud data is never mutated during the window, rollback is always clean.
Proof of "fully off shared"
After the flip, the shared-cloud load balancer shows zero requests carrying that tenant's API key across every block. That is the definitive independence check.
Engineering lessons
These were discovered and fixed on real graduations, and are now baked into the tooling. Read them before you plan your own.
1. A data re-sync silently reverts infra-config
The tenant's control row carries infrastructure settings — the auth JWT issuer URL, the API base URL, webhook URLs — and on shared they all point at shared-cloud hosts. A wholesale re-sync reverts the customer's own values back to the shared ones.
On one graduation this briefly broke login: the issuer reverted, so the customer's auth minted tokens with the shared issuer and then rejected them.
Fix: the migration captures the target's infra-config columns before the re-sync and restores them after, so customer values are never clobbered.
2. Env-var-driven apps: flipping code ≠ flipping URLs
A Next.js site bakes its NEXT_PUBLIC_* API URLs at build time from the hosting console's environment variables, not from the code repo. Deploying new code does not move them — you must update the console env vars and rebuild.
In one case, a website kept calling shared auth/forms/search after its code deploy, until its hosting-console env vars were flipped and it was rebuilt.
Angular apps that carry config in environment.prod.ts (code-config) don't have this trap — but know which model each of your apps uses before the window.
3. Sequences don't copy with rows
pg_dump / COPY moves data but not sequence state. The first insert after migration collides on the primary key.
Always advance every serial sequence to MAX(id) after a copy.
4. The JWT issuer must be the customer's own host
If the issuer doesn't match the validating host, logins bounce — the token is issued for one host and validated against another. For backends that can't flip in lockstep, a dual-trust issuer window (accept both the old and new issuer for a period) smooths the transition.
5. Watch for config drift across blocks
Per-tenant values that must be identical across every block database — the tenant API key, webhook URLs — tend to drift. Verify consistency and align every block to a single value before cutover.
Staging environments (advanced add-on)
Because In-a-Box runs in your own cloud account, you can add a full staging environment — a second, isolated copy of your blocks (same images, separate databases and hosts) for testing changes before production. This is a paid add-on available on any own-account deployment, operated by 23blocks or by your team. The Shared Platform is production-only and does not include staging.
Hybrid graduations
Graduation does not have to be all-or-nothing. A customer can keep select blocks on shared 23blocks while running the rest on their own infrastructure. This is a supported, deliberate choice — not a limitation — and is common when a particular block isn't in scope for the move.
Outcome
A completed graduation looks like this: the customer runs the identical 23blocks platform entirely on their own cloud account, on open-source-forked code they own, with their data migrated and parity-verified, every consumer surface cut over, and zero traffic left on shared services (verified). Rollback stays armed on every consumer throughout, and total user-facing disruption is a single maintenance window.
Related
- 23blocks in a Box — overview (the business case and graduation narrative)
- Deployment options (shared, dedicated, self-managed)
- Migration guide
- Self-managed deployment guide