C · Building a Game · Next steps

Production deployment

Everything so far ran under the orchestrator — a development tool. On a real Linux server you run the pieces yourself. It's less magic than it sounds: two processes, a database, and a web server.

▼ details below
The 60-second version (what we'll cover in the live session)

The target picture

playersbrowser + wallet your Linux server nginx :443 TLS + reverse proxy / → static files /api /batcher → inside static frontend game.html + built assets on disk sync node · systemd bun main.mainnet.ts → API :9999 batcher · systemd bun batcher.mainnet.ts → :3334 PostgreSQL real DB — state survives restarts .env secrets, chmod 600 EVM RPCe.g. Arbitrum One Midnightnode + indexer
two systemd processes + Postgres behind nginx; the chains are someone else's problem now

Dev vs production, piece by piece

In dev (orchestrator)In production (you)
local Hardhat chainsreal networks via an RPC provider
local Midnight node/indexer/proof serverMidnight network endpoints
PGLite, in-memory, wiped each runPostgreSQL, persistent, backed up
contracts compiled + deployed each bootdeployed once; addresses go in config
processes supervised by the orchestratortwo systemd units
main.dev.ts / config.dev.tsmain.mainnet.ts / config.mainnet.ts

Notice what's not in the table: your game code. grammar.ts, state-machine.ts, game.ts, and api.ts run unchanged — main.mainnet.ts imports the very same files.

1Database first

Dev used PGLite (PGLITE=true). Production points the same code at a real Postgres through env vars read by @effectstream/db:

sudo apt install postgresql
sudo -u postgres createuser gridbeasts -P
sudo -u postgres createdb -O gridbeasts gridbeasts

Migrations still apply automatically at node startup — but now state persists. (A restart no longer replays from block 0; the node resumes where it stopped.)

2The .env file

/opt/gridbeasts/.envchmod 600, owned by the service user, never committed:

# chain access
EVM_RPC_URL=https://arb1.arbitrum.io/rpc      # or your provider's URL
EVM_START_BLOCK=250123456                     # block where YOUR contracts were deployed
MIDNIGHT_START_BLOCK=1234567
# NTP_START_TIME=…                            # optional, ms epoch; else recovered from DB

# secrets
EVM_PRIVATE_KEY=0x…                           # batcher wallet — fund it, it pays the gas
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=gridbeasts
DB_USER=gridbeasts
DB_PW=…                                       # PGLITE stays UNSET in production
Start blocks matter
EVM_START_BLOCK = the block your contracts were deployed at, not 0 — or your first sync crawls the chain's entire history. Same logic for MIDNIGHT_START_BLOCK. And the batcher key is a hot wallet: fund it modestly, monitor it, rotate it if leaked.

3Config changes

config.mainnet.ts is the env-driven twin of config.dev.ts. It refuses to start without EVM_RPC_URL and the start blocks — fail-fast beats syncing the wrong chain. You edit it for the things that are facts about your deployment:

4Two systemd units

/etc/systemd/system/gridbeasts-node.service:

[Unit]
Description=GridBeasts sync node
After=network-online.target postgresql.service

[Service]
User=gridbeasts
WorkingDirectory=/opt/gridbeasts/app
EnvironmentFile=/opt/gridbeasts/.env
ExecStart=/usr/local/bin/bun run packages/node/main.mainnet.ts
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

gridbeasts-batcher.service is identical except ExecStart=… packages/batcher/batcher.mainnet.ts and After=… gridbeasts-node.service. Then:

sudo systemctl enable --now gridbeasts-node gridbeasts-batcher
journalctl -u gridbeasts-node -f        # your new `orchestrator logs sync`

5Static frontend

bun run --filter @evm-midnight/frontend build:mainnet   # → packages/frontend/dist
cp game.html packages/frontend/dist/                     # our game rides along

Edit the CONFIG block at the top of game.html: API and BATCHER become your domain (https://game.example.com/api …), ERC721 and CHAIN_ID become the real deployment. Copy dist/ to /opt/gridbeasts/www — it's just files; any CDN works too.

6nginx reverse proxy

server {
    listen 443 ssl;
    server_name game.example.com;
    # certbot manages the certs

    root /opt/gridbeasts/www;                 # static frontend

    location /api/ {
        proxy_pass http://127.0.0.1:9999;     # sync node (read-only API)
    }
    location /batcher/ {
        proxy_pass http://127.0.0.1:3334/;    # note trailing slash: strips the prefix
    }
}

One domain, TLS in one place, and ports 9999/3334 never face the internet — firewall everything except 443 (and your SSH). The batcher URL players sign against is now https://game.example.com/batcher.

Checkpoint — production smoke test
curl -s https://game.example.com/api/cards     # [] or your cards
systemctl status gridbeasts-node gridbeasts-batcher
journalctl -u gridbeasts-node | grep MIGRATION  # migrations applied against real Postgres
Then the real test: restart the server. systemd brings both processes back, Postgres still has the state, and the node resumes syncing from where it stopped.
What we deliberately skipped (ask in the live session)
Monitoring (the node exposes logs/metrics hooks), DB backups, running your own Midnight infrastructure vs. public endpoints, multiple node replicas behind one proxy, and zero-downtime deploys. All real, all solvable, none needed for your first deployment.
← 6 · Midnight seasonsWhere to go next →