Deployment
Deployment — Solana Token Distribution Protocol
Section titled “Deployment — Solana Token Distribution Protocol”Building, testing, and deploying the Solana program, API, and web frontend.
Contents
Section titled “Contents”- Prerequisites
- Building
- Testing
- Program deployment
- API deployment
- Frontend deployment
- CI/CD
- Browser compatibility
Prerequisites
Section titled “Prerequisites”- Rust —
rustup install stable|- Solana CLI — v3.1.12 - Anchor CLI — v0.32.1
- Node.js v24+ + pnpm (install:
corepack enable && corepack prepare pnpm@latest --activate) - Wrangler CLI — for API deployment (installed as devDependency)
Building
Section titled “Building”# Install dependencies for all workspacespnpm install
# Build everything (packages + program)pnpm buildTargeted scripts
Section titled “Targeted scripts”| Script | What it does |
|---|---|
pnpm program:build | Builds the Anchor program, syncs the IDL into the SDK, builds SDK |
pnpm sdk:sync | Copies the latest IDL from the program build into the SDK package |
pnpm sdk:build | Builds only the SDK package |
To get the program ID after building:
cd apps/solana-tdp-anchoranchor keys listUpdate declare_id!("...") in programs/solana-tdp/src/lib.rs and [programs.localnet] in Anchor.toml with the output, then rebuild.
Testing
Section titled “Testing”Tests run on Anchor LiteSVM with LiteSVM underneath — no local validator needed.
# Run all tests (program + SDK)pnpm test
# Or just the program tests using anchorcd apps/solana-tdp-anchoranchor test
# Web app unit + storybook testspnpm --filter @simplyvest/dapp test
# Typecheck everythingpnpm check:ts:allTest files
Section titled “Test files”| File | What it tests |
|---|---|
solana-tdp.000.create-stream.test.ts | Stream creation with valid/invalid parameters |
solana-tdp.001.withdraw.test.ts | Claiming vested tokens, cliff checks, partial claims |
solana-tdp.002.cancel.test.ts | Mid-stream cancellation, vested/unvested split |
solana-tdp.003.milestone.test.ts | Milestone stream creation, trigger, withdraw, cancel |
solana-tdp.005.security-audit.test.ts | 19 security audit tests — signer authority, PDA uniqueness, overflow, account ownership, state transitions, wrong-account attacks, timestamp boundaries |
fixtures.ts | Shared test fixtures (token mints, accounts, PDAs) |
Program deployment
Section titled “Program deployment”cd apps/solana-tdp-anchor
# Deploy to devnetpnpm run deploy
# Verifysolana program show 6VkmhxbTH9dnzAE7Scpxn6R3HeXYtY4oZffAFMAYvECk --url devnetProgram ID: 6VkmhxbTH9dnzAE7Scpxn6R3HeXYtY4oZffAFMAYvECk
Deployment info
Section titled “Deployment info”| Network | Solana Devnet |
| Program ID | 6VkmhxbTH9dnzAE7Scpxn6R3HeXYtY4oZffAFMAYvECk |
| Explorer | Solana Explorer (devnet) |
API deployment
Section titled “API deployment”The API (apps/api) is a Cloudflare Worker with D1 database.
One-time setup
Section titled “One-time setup”- Create D1 database:
Terminal window cd apps/apipnpm wrangler d1 create simplyvest-db - Copy the
database_idfrom output intowrangler.toml - Set secrets:
Terminal window pnpm wrangler secret put PRIVY_APP_IDpnpm wrangler secret put PRIVY_APP_SECRET - Apply migrations:
Terminal window pnpm db:migrate:remote
Deploy
Section titled “Deploy”pnpm deploy:apiLocal development
Section titled “Local development”pnpm dev:api # Start API on localhost:8787pnpm dev:dapp # Start dapp on localhost:5173pnpm dev # Start API, web, docs, and storybook in parallel
pnpm db:generate # Generate new migration from schema changespnpm db:migrate # Apply migrations to local D1pnpm db:reset # Drop all tables (local only)API endpoints
Section titled “API endpoints”| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/streams | — | Record new stream |
GET | /api/streams | — | List streams |
GET | /api/streams/:id | — | Get stream + events |
POST | /api/streams/:id/sync | — | Sync stream events |
POST | /api/streams/:id/events | — | Record stream event |
GET | /api/users/me | JWT | Get own profile |
POST | /api/users/me | JWT | Create profile |
PUT | /api/users/me | JWT | Update profile |
GET | /api/users/:id | — | Get public profile |
POST | /api/orgs | JWT | Create organization |
GET | /api/orgs/:id | — | Get org + members |
PUT | /api/orgs/:id | JWT | Update org |
DELETE | /api/orgs/:id | JWT | Delete org |
POST | /api/orgs/:id/members | JWT | Add member |
DELETE | /api/orgs/:id/members/:userId | JWT | Remove member |
GET | /api/orgs/me/list | JWT | List user’s orgs |
POST | /api/reconcile | JWT | Trigger reconciliation |
GET | /api/reconcile/stats | JWT | Reconciliation stats |
POST | /api/waitlist | — | Legacy waitlist |
GET | /api/tokens/r2/* | — | Serve R2 metadata JSON |
POST | /api/tokens/upload-image | JWT | Upload token image to R2 |
POST | /api/tokens/metadata | JWT | Store token metadata |
POST | /api/tokens | — | Record token info |
GET | /api/tokens | — | List tokens |
PATCH | /api/tokens/:mint/visibility | — | Toggle token visibility |
GET | /api/tokens/preferences | — | Get user token preferences |
POST | /api/tokens/create-platform | JWT | Create platform token on-chain |
PUT | /api/orgs/:id/token | JWT | Set org token (create or link) |
DELETE | /api/orgs/:id/token | JWT | Remove org token |
Docs deployment
Section titled “Docs deployment”The documentation site (apps/docs) is an Astro + Starlight static site, deployed to Cloudflare Pages.
Production URL: simplyvest-docs.pages.dev
One-time setup
Section titled “One-time setup”- Create the Pages project (already done):
Terminal window pnpm wrangler pages project create simplyvest-docs --production-branch=main
Local manual deploy
Section titled “Local manual deploy”# Build and deploypnpm --filter @simplyvest/docs buildpnpm --filter @simplyvest/docs deployThe deploy script runs wrangler pages deploy dist --project-name=simplyvest-docs from the package directory.
No secrets, API keys, or environment variables are needed — the docs site is pure static content.
Frontend deployment
Section titled “Frontend deployment”The React frontend (apps/dapp) is deployed to Cloudflare Pages via GitHub Actions. A push to main that touches apps/dapp/ or packages/solana-tdp-sdk/ triggers an automatic build and deploy.
Production URL: simplyvest.pages.dev
Preview deploys are created for pull requests at <branch>.simplyvest.pages.dev.
One-time setup
Section titled “One-time setup”-
Create a Cloudflare API token: Dashboard > API Tokens > Create Token > Custom >
Account > Cloudflare Pages > Edit -
Get your Account ID from the Cloudflare dashboard sidebar
-
Create the Pages project:
Terminal window pnpm dlx wrangler pages project create simplyvest --production-branch=main -
Add two GitHub Actions secrets:
CLOUDFLARE_API_TOKEN— your Cloudflare API tokenCLOUDFLARE_ACCOUNT_ID— your Cloudflare Account ID
Where to scope the secrets:
- Repository level (recommended) — Settings > Secrets and variables > Actions > Secrets. Available to all workflows without extra config.
- Environment level — If scoped to an environment (e.g.,
main), the workflow job must declareenvironment: mainor the secrets won’t be visible. Our CI workflow’sdeploy-webjob already includes this declaration.
Local manual deploy
Section titled “Local manual deploy”# Build and deploypnpm --filter @simplyvest/dapp buildpnpm --filter @simplyvest/dapp deployThe deploy script runs wrangler pages deploy dist --project-name=simplyvest from the package directory.
SPA routing works out of the box — Cloudflare Pages auto-detects a client-side router when there is no 404.html and serves index.html for all unmatched paths.
Storybook deployment
Section titled “Storybook deployment”The Storybook component library (apps/storybook) is deployed to Cloudflare Pages as a standalone static site.
Production URL: simplyvest-storybook.pages.dev
One-time setup
Section titled “One-time setup”- Create the Pages project (already done):
Terminal window pnpm wrangler pages project create simplyvest-storybook --production-branch=main
Local manual deploy
Section titled “Local manual deploy”# Build SDK (stories import from @solana-tdp/sdk)pnpm --filter @solana-tdp/sdk build
# Build and deploypnpm --filter @simplyvest/storybook build-storybookpnpm --filter @simplyvest/storybook deployThe deploy script runs wrangler pages deploy storybook-static --project-name=simplyvest-storybook from the package directory.
Each job has its own reusable workflow file in .github/workflows/, called by the orchestrator ci.yaml:
| Workflow file | Job | Triggers | What it does |
|---|---|---|---|
ci.yaml | (orchestrator) | Push/PR to main | Calls all other workflows, deploys on push |
lint.yaml | lint | PRs + main | JS/TS lint with oxlint |
format.yaml | format | PRs + main | Format check with oxfmt |
typecheck-web.yaml | typecheck-web | PRs + main | TypeScript check (web) |
typecheck-api.yaml | typecheck-api | PRs + main | TypeScript check (API) |
typecheck-sdk.yaml | typecheck-sdk | PRs + main | TypeScript check (SDK) |
typecheck-anchor.yaml | typecheck-anchor-ts | PRs + main | TypeScript check (anchor) |
typecheck-docs.yaml | typecheck-docs | PRs + main | Astro check (docs) |
test-api.yaml | test-api | PRs + main | API tests with vitest |
test-web.yaml | test-web | PRs + main | Unit tests with vitest (jsdom) |
test-e2e.yaml | test-e2e | PRs + main | Playwright e2e tests (Chromium, 11 specs) |
test-storybook.yaml | test-storybook | PRs + main | Storybook interaction tests (Playwright) |
build-web.yaml | build-web | PRs + main | Production build of React frontend |
rust-lint.yaml | lint-rust | PRs + main | cargo fmt + clippy |
anchor.yaml | anchor | PRs + main | Build Anchor program + vitest tests |
deploy-web.yaml | deploy-web | main only | Build SDK + Deploy web to Cloudflare Pages |
deploy-api.yaml | deploy-api | main only | Build SDK + Deploy API Worker + D1 migrations |
deploy-docs.yaml | deploy-docs | main only | Build + Deploy docs to Cloudflare Pages |
deploy-storybook.yaml | deploy-storybook | main only | Build SDK + Build + Deploy storybook to Cloudflare Pages |
GitHub Actions Variables
Section titled “GitHub Actions Variables”Set in Settings → Secrets and variables → Actions → Variables (repo level):
| Variable | Value | Used by |
|---|---|---|
VITE_API_URL | API worker URL (e.g., https://simplyvest-api.workers.dev) | deploy-web, build-web |
VITE_PRIVY_APP_ID | Privy App ID | deploy-web, build-web |
VITE_PRIVY_CLIENT_ID | Privy Client ID | deploy-web, build-web |
VITE_GA_MEASUREMENT_ID | Google Analytics ID | deploy-web, build-web |
VITE_SENTRY_DSN | Sentry DSN (optional) | deploy-web |
GitHub Actions Secrets
Section titled “GitHub Actions Secrets”Set in Settings → Secrets and variables → Actions → Secrets (environment main):
| Secret | Value | Used by |
|---|---|---|
CLOUDFLARE_API_TOKEN | Cloudflare API token (Pages + Workers Edit) | deploy-web, deploy-api, deploy-docs, deploy-storybook |
CLOUDFLARE_ACCOUNT_ID | Cloudflare Account ID | deploy-web, deploy-api, deploy-docs, deploy-storybook |
Why environment-scoped? The deploy jobs declare environment: main, so they can access these secrets. PR workflows don’t have access — this prevents a malicious PR from leaking credentials.
Setting secrets/variables via CLI
Section titled “Setting secrets/variables via CLI”# Variables (repo level)gh variable set VITE_API_URL --body "https://simplyvest-api.workers.dev"gh variable set VITE_PRIVY_APP_ID --body "your-app-id"gh variable set VITE_PRIVY_CLIENT_ID --body "your-client-id"gh variable set VITE_GA_MEASUREMENT_ID --body "G-XXXXXXXXXX"
# Secrets (environment level)gh secret set CLOUDFLARE_API_TOKEN --env maingh secret set CLOUDFLARE_ACCOUNT_ID --env main
# Verifygh variable listgh secret list --env mainBrowser compatibility
Section titled “Browser compatibility”Solana’s web3.js and wallet adapter libraries use Node.js globals (Buffer, process, global) that do not exist in browsers. The dapp frontend polyfills them in apps/dapp/app/main.tsx:
import { Buffer } from "buffer";import process from "process";globalThis.Buffer = Buffer;globalThis.process = process;The Vite config also maps global to globalThis:
define: { global: "globalThis",}If a new dependency triggers a crypto is not defined or stream is not defined error at runtime, add the corresponding alias to vite.config.ts:
| Missing module | Polyfill package | Vite alias |
|---|---|---|
crypto | crypto-browserify | resolve: { alias: { crypto: "crypto-browserify" } } |
stream | stream-browserify | resolve: { alias: { stream: "stream-browserify" } } |
These are rarely needed — most Solana wallet adapters have browser-native fallbacks. Only add them if you hit actual runtime errors.