Skip to main content

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:

VariableDescription
DB_HOSTPostgreSQL hostname
DB_NAMEDatabase name
DB_USERNAMEDatabase user
DB_PASSWORDDatabase password
SECRET_KEY_BASERails secret key (64+ hex chars)
REDIS_URLRedis URL with DB number, e.g. redis://host:6379/0
ENCRYPTION_SERVICE_KEYPer-tenant encryption master key (fail-fast)
ENCRYPTION_SERVICE_SALTEncryption 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_HOST for 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. See ENV-VARS.md for the complete per-block set.

Common optional variables​

VariableDefaultDescription
RAILS_ENVproductionRails environment
DB_PORT5432PostgreSQL port
PORT80HTTP listen port (Puma)
SENTRY_DSN(none)Error tracking; omit to disable
RAILS_MAX_THREADS5Puma thread pool size
WEB_CONCURRENCY2Puma worker count
RAILS_LOG_TO_STDOUTtrueSend 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.

EndpointPurpose
GET /healthBasic liveness check
GET /health/readyReadiness (DB + Redis connected)
GET /health/detailedFull 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 Platform 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:

  1. Auth — sign in, then validate the token.
  2. The full read path.
  3. 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 Platform data is never mutated during the window, rollback is always clean.

Proof of "fully off shared"​

After the flip, the Shared Platform 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 Platform 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.