Deployment Guide
This walks through bringing the full Veil stack online from a clean clone: program, database, dApp, optional docs site.
Prerequisites
| Requirement | Version |
|---|---|
| Node.js | ≥ 18 (Next.js 16 requires modern Node) |
| Rust + cargo | stable; Solana CLI ships its own toolchain for SBF |
| Solana CLI | ≥ 1.18 — provides cargo build-sbf |
| Postgres | A Neon project (or any Postgres ≥ 14 reachable over TLS) |
| Wallet | A funded Solana keypair for deploys |
1. Build & deploy the program
Build
cd programs
cargo build-sbfThis produces programs/target/deploy/veil_lending.so.
Deploy
Deploy commands default to your solana config get URL. Always pass
--url devnet (or --url localhost) explicitly. Never deploy a build
with --features testing to mainnet — it ships the MockOracle / MockFees
instructions and a hardcoded mock-admin override.
solana program deploy \
programs/target/deploy/veil_lending.so \
--url devnet \
--keypair ~/.config/solana/id.jsonSolana CLI prints the deployed program ID. Save it.
Verify
solana program show <program-id> --url devnetShould report a non-zero data size and your keypair as the upgrade
authority. The program is now callable; no further on-chain bootstrap is
needed beyond per-pool Initialize calls.
2. Run protocol tests
cd programs
cargo test --lib # 105 unit tests across math, state, accrual
cargo test --tests # 57 + 99 + 81 across instruction parsing, scenarios, regressionsAll tests must pass. They cover every formula, every PDA derivation, and every state transition. A failing test is a deploy blocker.
3. Set up the database
Provision Neon (or any Postgres)
Create a project at neon.tech . Copy the pooled
connection string — it must include ?sslmode=require.
Cluster-scoped rows, single DB OK in dev. Tables that hold on-chain
state (pools, positions, tx_log, audit_log) carry a cluster
column. The dApp filters reads and stamps writes with the active
NEXT_PUBLIC_SOLANA_CLUSTER, so you can point all three local builds at
one Neon project without rows colliding. For production you should
still point mainnet at its own dedicated database — column scoping makes
mistakes loud rather than catastrophic, but full isolation is cheaper
insurance than a recovery.
Configure environment
Create veil-landing/.env.local (gitignored). Required keys:
# Pins the build to a single cluster. On Vercel set this per environment
# (Production = mainnet, Preview = devnet).
NEXT_PUBLIC_SOLANA_CLUSTER=devnet
# Program ID for the active cluster. Next.js inlines this at build time, so
# each Vercel environment carries the deploy id matching its cluster.
NEXT_PUBLIC_VEIL_PROGRAM_ID=<deploy id for this cluster>
# Server-only secrets. One DATABASE_URL is enough — rows are scoped by the
# `cluster` column. For production, use a dedicated mainnet database.
DATABASE_URL=postgresql://USER:PASS@HOST/veil?sslmode=require
SUPER_ADMIN_PUBKEY=<base58 wallet>
# Server-side RPC overrides (defaults: public Solana RPCs / 127.0.0.1:8899)
MAINNET_RPC=
DEVNET_RPC=
LOCALNET_RPC=
# Optional per-asset mints (overrides defaults in lib/veil constants)
NEXT_PUBLIC_SOL_MINT=So11111111111111111111111111111111111111112
NEXT_PUBLIC_USDC_MINT=4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
# ...etcDATABASE_URL and SUPER_ADMIN_PUBKEY are server-only. Do not prefix
either with NEXT_PUBLIC_ — that would leak the secret into the client
bundle.
Run the migration
cd veil-landing
npm install
npm run db:migrateThe migration:
- Reads
lib/db/schema.sqland applies allCREATE TABLE … IF NOT EXISTS. - Inserts/updates the bootstrap super-admin from
SUPER_ADMIN_PUBKEY.
Re-running is safe — every table uses IF NOT EXISTS, the seed uses
ON CONFLICT DO UPDATE SET role='super_admin', revoked_at=NULL.
Add additional admins (optional)
npm run db:add-admin -- <pubkey> [pool_admin|super_admin] [label]Direct DB insert — bypasses the signed-nonce flow. Use it for bootstrapping or recovery; for routine adds prefer the UI.
4. Run the dApp
Development
cd veil-landing
npm run dev # next dev on :3000 by default
PORT=4321 npm run dev # if 3000 is takenOpen http://localhost:<port>. The first request takes a few seconds while
Next compiles the route lazily.
Production build
npm run build
npm run start # next start on :3000next build runs both Webpack/Turbopack compilation and the TS type-check.
A clean build means every route compiles, every type lines up, and every
MDX in this docs site parses.
The expected build summary lists 14 routes — three pages (/, /dapp,
/dapp/admin, /dapp/liquidate, /workflow), seven API routes, and the
not-found page.
5. Initialize a pool
Once the dApp is running and the deploying wallet has been added to
pool_admins (the bootstrap wallet is automatically added by the
migration):
- Open
/dapp/admin. The Authorized banner should show. - Click the Initialize Pool tab.
- Paste the SPL token mint. Optional symbol for display.
- Submit. Sign two transactions:
- The dApp builds a single tx with two ix:
createAssociatedTokenAccountfor the vault, thenInitialize. - After confirmation, sign a nonce so the server can register the pool.
- The dApp builds a single tx with two ix:
The pool is now in pools and visible in the Manage Pools tab.
6. Run the docs site (optional)
cd docs
npm install
npm run dev # nextra dev on :3001
npm run build # production buildThe docs site reads MDX from docs/content/ and is built with Nextra 4.6.
This page lives at docs/content/integration/deployment.mdx.
Production hardening checklist
Before pointing real users at a Veil deployment:
- Replace single-keypair
pool.authoritywith a Squads multisig vault PDA. - Front the multisig with a governance program that imposes a timelock on
UpdatePool. - Rotate
DATABASE_URLon team membership changes; treat the connection string as a high-trust secret. - Configure a Pyth price feed for each pool and call
UpdateOraclePriceat least once to anchor it. After anchoring, the pool will reject any attempt to swap to a different feed. - Run a keeper that calls
UpdateOraclePriceregularly (e.g. every minute during volatility) and atomically with any sensitive instruction. - Write an indexer that populates
positionsfrom on-chainUserPositionaccounts so the liquidator UI can scan unhealthy positions efficiently. - Audit. The protocol is pre-audit in v0.1.
Troubleshooting
bigint: Failed to load bindings, pure JS will be used — benign warning
from bigint-buffer (transitive dep of @solana/spl-token). Falls back to
pure JS. Ignore.
Hydration mismatch on WalletMultiButton — confirm pages import
WalletButton from app/components/WalletButton.tsx rather than the raw
adapter export. The wrapper renders a placeholder during SSR + first paint.
relation "pool_admins" does not exist — your npm run db:migrate ran
against an empty database but the schema didn’t apply. The Neon HTTP
serverless driver doesn’t support multi-statement DDL; the migration uses
the WebSocket Pool driver via ws. Confirm ws is installed
(npm install --save-dev ws @types/ws) and neonConfig.webSocketConstructor = ws is set in lib/db/migrate.ts.
The fetchConnectionCache option is deprecated — harmless deprecation
notice from a newer Neon serverless version. Already removed from
lib/db/index.ts; if you re-add it, delete it again.