Skip to main content

Deploying staging

Staging is the whole system standing and kept current — Sepolia contracts, a Supabase project, the indexer on a Contabo box, the web on Vercel (D33). This is the state of each piece and the order they have to go in. It is not a plan: the plan is PLAN.md §7.

Nothing here is production. Production is Arbitrum One, a prod branch and a second Supabase project, and it is not in this phase.

The order, and why it is fixed

Each step produces what the next one needs, so this order is a dependency, not a preference.

StepProducesNeeded by
1Sepolia contractscontracts/deploy/421614.json with a Lens and the quote menueverything
2Supabaseproject URL, publishable key, DATABASE_URLindexer, web
3Contabothe indexer running against Sepoliathe web's numbers
4Vercelthe site
5CIservices.yml and supabase.yml proven green— (do any time)

What the developer has to supply

None of this may enter the repository.

Alchemy Sepolia URLARB_SEPOLIA_RPC locally for the deploy; RPC_HTTP on the VPS; the metadata function's chain list is hard-coded to the public endpoint and is fine as it is
Contabo IP and root passwordFor the first login only. The box ends up key-only
Supabase projectAlready linked: ref vbtioxfmmayjbhnbhzsp in supabase/.temp/project-ref. Its database password, and its keys
Deployer keystoretest-arb-launchpad-deployer, already holding Sepolia ETH at 0x9e0A…d900. Never a raw key (D6)

1. Sepolia contracts

Settled 21 Sep 2026 by the developer: a completely fresh deploy. Not an upgrade of what is there. The reasons, so this is not reopened:

  • What is on Sepolia runs code with known bugs — S2 and S3 from the security pass, fixes on master and never shipped. An upgrade leaves those launches on that cohort for life (D12), in the catalog, in the thing people are being asked for feedback on.
  • It has no Lens, so app/scripts/sync.mjs refuses to build the site against it at all.
  • It is paired against WETH only, not the ARB and RHUB stand-ins the interface is written for.
  • A recent deployBlock means a short backfill, which matters on a free RPC.

Nothing on Sepolia has value, so the cost is only that the old launches disappear from the book.

ARB_SEPOLIA_RPC=<alchemy> make sepolia-rehearse # simulates against real state, sends nothing
ARB_SEPOLIA_RPC=<alchemy> make sepolia-deploy # signed by the keystore

Rehearse first and state the expected cost before sending (hard rule). The script checks bytecode at thirteen addresses afterwards, because the book is written during simulation and a half-landed broadcast reads exactly like success. Then it records deployBlock from the receipts — block.number is the L1 block on Arbitrum and a forge script cannot know the L2 one.

What a fresh deploy gives you, with no further steps. Deploy.s.sol on 421614 deploys ARB and RHUB stand-ins (neither exists on Sepolia, D39), registers them as the quote menu, enables all six modules, and opens launching. The book it writes carries lens and a recent deployBlock.

Consequences to carry through the rest of the deployment

New factory addressThe metadata function's FACTORY_ADDRESSES secret must be the new one, or every metadata write is rejected as an unknown chain
Old launch_metadata rows are orphanedKeyed to tokens from a factory nothing points at. Harmless; delete them if the table should read clean
Commit the bookcontracts/deploy/421614.json is committed (D29) and is what both the site and the indexer build from

Testers need the quote assets

The stand-ins are mintable by anyone on purpose — a quote nobody can obtain is a launchpad nobody can use — but the interface has no affordance for it, so a tester with an empty wallet can look and not trade. mint(address,uint256) is unguarded on both.

Thresholds are 69,000 ARB and 250,000 RHUB, so a launch is not graduated by one person on a whim.

Decide in the session: a faucet button in the app, or a documented cast send. The first is a small change and is the difference between feedback on the whole flow and feedback on the catalog.

Seeding

A staging launchpad with four launches gathers no feedback about a catalog. DevSeed.s.sol is local-only today. Decide whether to adapt it for Sepolia or to launch by hand through the new interface — the second doubles as the wallet-write-path test below.

2. Supabase

The project is already linked and it is not empty — it has served launch_metadata for the old interface. That is the one real hazard in this deployment: D35 exists because migration history has had to be repaired by hand before.

Read before writing. supabase migration list compares local against remote and says exactly which of the six the project already has. Do that first; decide nothing before reading it.

  • If it has 20260918000000 and nothing else, supabase db push applies the five indexer migrations and there is no conflict.
  • If it has that table but no migration row for it, the push fails on the first statement. Repair is supabase migration repair --status applied 20260918000000, not a hand-edit.

Then, in order:

supabase db push # the five indexer migrations
supabase gen types typescript --linked | diff - supabase/types/database.types.ts # compare, never overwrite
make supabase-deploy # the metadata and auth functions

Compare the types, do not write them. --linked emits an __InternalSupabase block naming the remote PostgREST version that --local does not, so piping it over the committed file introduces a difference CI can never agree with: make supabase-check regenerates from --local and diffs. Everything below that block being identical is what "unchanged" means.

Secrets the functions need (supabase secrets set), none of which have defaults worth having:

JWT_SECRETThe project's own JWT secret. The auth function signs sessions with it
FACTORY_ADDRESSES{"421614":"0x…"} — the metadata function refuses a chain it has no factory for
ALLOWED_ORIGINhttps://stage.stadium.money. It defaults to *, which is not what staging should run

SUPABASE_URL and the secret key are injected by the platform.

3. Contabo

The box exists; it is root-and-password today. Harden before anything else runs on it — a key, a non-root user, password and root login disabled, a firewall. Then Docker and Compose.

docker-compose.staging.yml is written and takes everything from the environment:

CHAIN_ID=421614 RPC_HTTP RPC_WS DATABASE_URL FACTORY LENS POOL_MANAGER START_BLOCK

START_BLOCK is deployBlock from the address book. DATABASE_URL is the Supabase pooler URL, not the direct one.

The image builds on the box. services/indexer/Dockerfile exists and a cold Rust build is minutes; that is fine once, on a machine nobody is waiting on. Do not put that build in the dev loop.

Use the public RPC, not Alchemy. This was the wrong way round in an earlier draft. Alchemy's free tier serves eth_getLogs over ten blocks on Arbitrum Sepolia and then throttles on compute units per second; the public endpoint answered the whole 12,553-block backfill in one call. Pay for Alchemy or use the public endpoint — the free tier is neither. Alchemy stays the right choice for the deploy, where the public endpoint timed out mid-simulation.

The database URL is the session pooler, port 5432 — not the transaction pooler on 6543. sqlx keeps server-side prepared statements and transaction pooling rotates the backend underneath them, so 6543 fails immediately with prepared statement "sqlx_s_1" already exists. The host is aws-1-eu-west-1.pooler.supabase.com for this project, and the username is postgres.<ref> rather than postgres. Neither the direct host nor the API URL is what goes here.

Watch the first backfill. The indexer splits only on a size refusal, which is correct — but it only recognises the wordings it has been taught, and an unrecognised one crashes rather than narrowing. make indexer-logs is local only — on the box it is docker compose logs -f.

4. Vercel

A new project for app/, and the old one retired — decided 21 Sep 2026. web/ goes with it.

  • web/vercel.json is the template: framework vite, install from the repo root, an SPA rewrite, and an ignoreCommand. app/ needs its own.
  • :/-prefixed pathspecs in the ignore command. Vercel runs it from the repository root, so -- app matches nothing, git reports no changes and every deployment comes out Canceled.
  • VITE_CHAIN=421614 so sync.mjs reads the Sepolia book.
  • VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY from the staging project.
  • A stale VITE_* in the dashboard silently beats a generated file. Check what is set before blaming a build.

Retiring the old one means deleting the Vercel project and web/ from the repo, plus its workspace entry in the root package.json and any web/ path filter in CI.

make does not know app/ exists. test-web, build-web, fmt-web and every aggregate that depends on them still run against web/, so make check — what CI gates on — has never once typechecked the new interface. Whatever else happens to web/, those targets have to point at app/, or a broken build ships and CI stays green.

5. CI

services.yml and supabase.yml are written and have never run. They are path-filtered, so they fire on the first change under services/ or supabase/ — or workflow_dispatch now. The Supabase job is the one worth watching: it replays every migration from zero and fails if the committed types no longer describe the result (D35).

Before any of it

The wallet write path has never run. Buying, selling and launching are simulated against the local chain and correct there, but writeContract → receipt → invalidate has not once been exercised. Ten minutes with a wallet on the local chain, and much cheaper to find there than on Sepolia.