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.
| Step | Produces | Needed by | |
|---|---|---|---|
| 1 | Sepolia contracts | contracts/deploy/421614.json with a Lens and the quote menu | everything |
| 2 | Supabase | project URL, publishable key, DATABASE_URL | indexer, web |
| 3 | Contabo | the indexer running against Sepolia | the web's numbers |
| 4 | Vercel | the site | — |
| 5 | CI | services.yml and supabase.yml proven green | — (do any time) |
What the developer has to supply
None of this may enter the repository.
| Alchemy Sepolia URL | ARB_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 password | For the first login only. The box ends up key-only |
| Supabase project | Already linked: ref vbtioxfmmayjbhnbhzsp in supabase/.temp/project-ref. Its database password, and its keys |
| Deployer keystore | test-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
masterand 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.mjsrefuses 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
deployBlockmeans 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 address | The 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 orphaned | Keyed to tokens from a factory nothing points at. Harmless; delete them if the table should read clean |
| Commit the book | contracts/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
20260918000000and nothing else,supabase db pushapplies 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_SECRET | The 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_ORIGIN | https://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.jsonis the template: frameworkvite, install from the repo root, an SPA rewrite, and anignoreCommand.app/needs its own.:/-prefixed pathspecs in the ignore command. Vercel runs it from the repository root, so-- appmatches nothing, git reports no changes and every deployment comes out Canceled.VITE_CHAIN=421614sosync.mjsreads the Sepolia book.VITE_SUPABASE_URLandVITE_SUPABASE_PUBLISHABLE_KEYfrom 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.