Self-host & deploy

Stand up your own Arcora — env vars, infra, and the runbook.

The Arcora codebase is open source — github.com/arcoralabs/arcorapay. Most merchants will use the hosted version at arcorapay.xyz; if you need to self-host (compliance, branding, sovereignty), here's the shape.

Components

ServiceWhat it does
packages/app (Next.js on Vercel)Hosted checkout, merchant dashboard, REST API
packages/contracts (Foundry)ArcFXGateway — custody-escrow gateway. Live testnet deployment at 0x07BAC123…aE3a3 (recorded in packages/contracts/deployments/arc-testnet.json); deployed via script/Deploy.s.sol
ops/relayer (single VPS)Drains the Permit2 queue, runs kit.swap, calls settleInvoice
ops/indexer (same VPS)Watches gateway events, writes invoice status, enqueues webhooks
Postgres (Supabase or self-hosted)Invoices, queue, merchant rows, compliance audit log

Required env vars

App (packages/app)

POSTGRES_URL_NON_POOLING   direct DB URL (used by API + indexer; e.g. Supabase :5432)
POSTGRES_URL               pooled URL (used by Vercel functions; e.g. Supabase :6543)
GATEWAY_ADDRESS            custody-escrow gateway (current ArcFXGateway address)
NEXT_PUBLIC_GATEWAY_ADDRESS    same address, exposed to client components
USDC_ADDRESS               on Arc
EURC_ADDRESS               on Arc
ARC_TESTNET_RPC            https://rpc.testnet.arc.network
NEXT_PUBLIC_RELAYER_ADDRESS the relayer EOA you'll fund
KIT_KEY                    Circle App Kit API key
PUBLIC_BASE_URL            https://yourdomain
SESSION_SECRET             32+ bytes random; for SIWE cookie sealing
WEBHOOK_SECRET_AES_KEY     32 bytes; encrypts merchant webhook secrets at rest

Optional (compliance):

COMPLIANCE_PROVIDER          noop | elliptic | trmlabs   (default noop)
COMPLIANCE_API_KEY           required for non-noop
COMPLIANCE_FAIL_OPEN_FOR_PAY     default false (fail-closed for customer pay)
COMPLIANCE_FAIL_OPEN_FOR_INVOICE default true  (fail-open for invoice creation)

Relayer + indexer (VPS)

Same as app, plus:

RELAYER_PK             relayer wallet private key (encrypted in DB; ops/relayer derives)
INDEXER_TICK_MS        default 30000
INDEXER_REORG_BUFFER_BLOCKS  default 5
RELAYER_TICK_MS        default 5000

Database

Drizzle migrations live at packages/app/lib/db/migrations/. Two-step apply:

  1. vercel env pull .env.production.local (or copy your env)
  2. pnpm exec drizzle-kit migrate from packages/app

If the __drizzle_migrations table is out of sync (manual SQL applied earlier), run the SQL files directly inside a transaction and backfill __drizzle_migrations once so future migrations apply cleanly.

Deploying the gateway

cd packages/contracts
forge script script/Deploy.s.sol \
  --rpc-url $ARC_TESTNET_RPC \
  --broadcast --slow --verify \
  --verifier-url $ARC_EXPLORER_URL \
  --etherscan-api-key $ARC_EXPLORER_KEY

Then verify with cast receipt (status=1) and cast code (non-empty bytecode) before trusting the broadcast file, repoint every consumer env (Vercel + VPS daemons), and restart the daemons. We run an internal deploy checklist covering exactly that ground before every redeploy — do the same before mainnet.

Operational reality

  • Single-instance relayer — the canonical deploy runs one relayer on one VPS. Multi-relayer with rolling failover is on the v1.x ops list.
  • Foundry broadcast can lie on Arc testnet — always verify with cast receipt (status=1) AND cast code <addr> (non-empty bytecode).
  • Drizzle tracking on prod is empty — historical SQL was applied directly, so drizzle-kit migrate would try to re-apply 0000–0005. Backfill __drizzle_migrations once or keep applying SQL directly.