Financing & subscriptions management platform (monorepo).
Ballast supports multiple payment processors via adapter interfaces. Stored payment credentials are processor-specific (Stripe payment methods, Braintree tokens, etc.). If we ever want “route across processors without re-collecting credentials”, we’ll need a separate orchestration/vault layer.
Current integrations in this repo include Stripe (API + web), and client integrations for Braintree and Square (webapp).
apps/webapp(Next.js, port3001): payment forms + demo cart/checkout flows.apps/admin(Next.js, port3002): admin dashboard for users, plans, subscriptions, refunds, catalog, jobs.apps/api(Express, port3000): REST API, auth, webhooks, email sending, internal job endpoints.apps/jobs(Node): “job runner” code meant for AWS Batch/EventBridge; can also be run locally.
@ballast/shared is for sharing code, not dependencies. It includes:
- Database: Prisma schema + Prisma client (
packages/shared/src/db/client.js) - Money utilities: integer cents + basis points helpers (
packages/shared/src/money.js) - Env helpers: dotenv loading + one-time warnings (
packages/shared/src/config/env*.js) - Misc: shared styles, fonts, auth utils
If an app imports a dependency directly, that app must list it in its own package.json (even if @ballast/shared also uses it).
- Node:
>= 20 - pnpm:
>= 8(repo pinspnpm@8.15.0) - Postgres: available locally (Prisma uses
DATABASE_URL)
pnpm installEach app typically uses an .env.local file in its own directory.
- API:
apps/api/.env.localDATABASE_URL(Postgres connection string)JWT_SECRETWEBAPP_URL(defaulthttp://localhost:3001)ADMIN_URL(should behttp://localhost:3002for local dev; required for CORS)STRIPE_SECRET_KEY(required for Stripe flows)RESEND_API_KEY,RESEND_FROM_EMAIL(required to send emails)JOBS_INTERNAL_API_TOKEN(required if running Jobs -> API notifications)
- Jobs:
apps/jobs/.env.localJOBS_INTERNAL_API_TOKEN(must match API)API_INTERNAL_URL(optional; defaults toAPI_URLthenhttp://localhost:3000)
# API (auto-runs Prisma db sync, then starts in watch mode)
pnpm --filter @ballast/api dev# Admin dashboard (http://localhost:3002)
pnpm --filter @ballast/admin dev# Webapp payment forms (http://localhost:3001)
pnpm --filter @ballast/webapp devAdmin access: the admin UI requires the user record to have isAdmin: true.
Prisma lives in packages/shared/prisma/. For dev, the API’s pnpm --filter @ballast/api dev runs a sync step (push + generate) before starting.
cd packages/shared- Dev sync:
pnpm db:sync - Generate client:
pnpm db:generate - Create migration:
pnpm db:migrate - Apply committed migrations:
pnpm db:deploy - Studio:
pnpm db:studio - Reset (destructive):
pnpm db:reset - Seed (optional):
pnpm db:seed:catalog,pnpm db:seed:events
Jobs are in apps/jobs/lib/jobs/ and are runnable locally via scripts:
pnpm --filter @ballast/jobs runChargeFinancing
pnpm --filter @ballast/jobs runChargeSubscriptions
pnpm --filter @ballast/jobs runSendUpcomingChargeRemindersSome jobs request the API to send emails via internal endpoints protected by a shared secret.
- Auth: Jobs send
Authorization: Bearer <JOBS_INTERNAL_API_TOKEN>. API validates this viarequireInternalJobsAuth. - Base URL: Jobs use
API_INTERNAL_URL(fallbackAPI_URL, thenhttp://localhost:3000). - Reminder lead time:
PAYMENT_REMINDER_DAYS_BEFOREcontrols how many days before a scheduled charge we send reminders (default:3). - Routes (API): mounted at
POST /internal/notifications/*and include:/subscriptions/upcoming-charge/subscriptions/charge-failed/subscriptions/defaulted/financing/upcoming-charge/financing/charge-failed/financing/defaulted
- Money: store and compute money as integer cents only; percentages use basis points. Use
@ballast/shared/src/money.js. - Dates/time in logic: tests should not call
new Date()directly inside domain logic; inject a clock (clock.now()). - Env access: each app centralizes
process.envreads in itsconstants.jsand warns once when defaults are used. - Network requests (webapp/admin): components don’t call
fetch(); use the gateway pattern (component -> store/context -> gateway -> api route -> api gateway). - Route protection (webapp/admin):
AuthGuardis used at the page level only. - Import aliases: apps use
@/for app-local imports and@shared/for shared code. The API uses an ESM loader (apps/api/src/register.mjs,apps/api/src/loader.mjs) to make this work at runtime.
- Jobs:
pnpm --filter @ballast/jobs test(uses Node’s built-in test runner)
- Lint:
pnpm lint - Format:
pnpm format - Docs:
pnpm docs(JSDoc)
- Admin (Next.js) → Vercel
- Webapp (Next.js) → Vercel
- API (Express) → Vercel (exports the Express app; in local dev it listens on
PORTwhen not in Vercel runtime) - Jobs → AWS Batch (containerized) + EventBridge scheduling + Parameter Store
Ballast supports an admin-driven ban/unban system enforced at the API auth/account level.
- Immediate logout from all devices: existing JWT sessions are invalidated by rejecting tokens issued before a server-stored timestamp (see
tokensInvalidBeforebehavior in API auth). When a ban/unban occurs, that timestamp is advanced so existing sessions fail on next request. - Known limitation: this is not designed to prevent banned users from creating new burner accounts (IP-only blocking is noisy and bypassable).
- Webapp:
https://ballast.systems - Admin:
https://admin.ballast.systems - API:
https://api.ballast.systems - Mail:
https://mail.ballast.systems