ShipAgent

Developer reference

How the ShipAgent code is organized, the rules the payment and credit code follows, and where to read further in the repository.

This section is for whoever reads or changes the code: you, a developer you hire, or your agent when no skill covers the change. It describes what the code does today. When this page and the code disagree, the code wins.

The stack: Next.js 16, React 19, TypeScript 5, Drizzle ORM 0.44 on PostgreSQL (the public schema), Better Auth 1.4 with the organization plugin, next-intl 4 (eight locales: en, zh, ja, fr, de, ko, es, it, with English as the per-key fallback), Tailwind 4 with shadcn/ui, Resend or Cloudflare Email Sending, Upstash Redis, Cloudflare R2 and the AI SDK.

Pages in this section

Page What it covers
Payments Checkout, the three provider adapters, webhook routes, the fulfillment core, refunds, error handling
Credits The two buckets, the ledger API, the yearly drip and its cron, team pools, admin adjustments
Auth and database Better Auth setup and hooks, sessions, roles, impersonation, rate limits, the Drizzle schema, migrations, connection settings
CMS Blog and glossary posts, MDX files, the admin editor, AI translation, the sitemap
Email sendEmail, templates, which events send mail, newsletter, Discord notifications
Environment variables Every variable in .env.example, one line each

Directory map

Directory What lives there
actions/ Server Actions: billing, coupons, credits, orders, overview, paypal, team, users, feedback, newsletter, plausible, posts, r2-resources
app/ App Router: [locale] pages, (site) routes, and api/ (Stripe, Creem and PayPal webhooks, PayPal create-order and capture-order, cron/credits, health, auth, ai-demo, admin, webhooks)
components/ UI: pricing, layout, header, footer, home, cms, tiptap, ai-demo, shared, ui
config/ Business settings that are code, not database rows: pricing.ts, credits.ts, site.ts, ai-providers.ts, ai-models.ts, colors.ts, common.ts
lib/ Core libraries: db, credits, billing, auth, mail, redis, storage, stripe, creem, paypal, ai, cms, logger, tracking, pricing, signup-blocklist, signup-gift-gate
i18n/ next-intl routing and messages/<locale>/ (one namespace per file; request.ts merges each locale over English)
emails/ react-email templates
blogs/ MDX blog posts per locale (en, zh, ja ship with demo files)
content/ Static MDX pages
constants/, hooks/, stores/, types/, styles/ Shared constants, React hooks, zustand stores, TypeScript types, global CSS and theme tokens
scripts/ Ops scripts: bootstrap-stripe-prices, the check-* verifiers the skills end with, env sync, a PayPal renewal test, a read-only spend probe
skills/ Agent skills (setup-*, deploy-*, add-*) and skills/reference/; .claude/skills links here and AGENTS.md is the entry for Codex and Cursor
cloudflare/ The Cloudflare Workers layer (front Worker, cron table, site-data.json); see CLOUDFLARE.md

Architecture rules

These come from the <architecture> block of CLAUDE.md at the repo root. Every payment and credit change has to keep them true.

Pricing

  • config/pricing.ts is the single source of truth. There is no pricing_plans table and no admin screen for plans.
  • Three plan kinds: license (one-time, grants no credits), subscription (monthly or yearly, grants credits each month) and credit_pack (one-time, grants credits that never expire). ShipAgent itself sells two license plans (one year of updates, lifetime); the other two kinds are there for the product you build.
  • Each plan has one provider (stripe, creem or paypal) and per-provider price IDs as { test, live }. Test or live is picked from the key: sk_live_ or rk_live_ means live for Stripe, a creem_test_ key means test for Creem, NEXT_PUBLIC_PAYPAL_ENVIRONMENT=live means live for PayPal.
  • A plan id is never reused, and a plan that has been sold is never deleted: renewal and upgrade invoices of existing subscriptions are resolved through it. Retire a plan with active: false.

Credits

  • Two buckets: subscription credits (reset on each grant) and purchased credits (never expire). Spending drains the subscription bucket first.
  • Balances change only through lib/credits/ledger.ts. credit_transactions is an append-only log; each row carries the delta, a snapshot of both buckets after the change, and a seq number.
  • Yearly plans grant month one at once, then 11 monthly grants. A cron and a lazy settlement on read both deliver them, anchored on the subscription row.

Fulfillment

  • lib/billing/fulfillment.ts is the provider-agnostic core. The idempotency key is the unique index on orders(provider, provider_order_id).
  • Each adapter (lib/billing/stripe.ts, creem.ts, paypal.ts) turns provider webhook events into core calls and maps the provider's subscription status to one shared set.
  • A mid-cycle plan change goes through incremental fulfillment. A pending payment (a PayPal eCheck) lands as an order with status='pending' and is reconciled by the cron.
  • Webhook routes stay thin. A retryable error answers 5xx so the provider sends the event again; a PermanentFulfillmentError sends an alert and then answers 2xx.

Teams

  • Teams are Better Auth organizations. The organization is the billing subject: a team subscription fills the shared pool in organization_credit_balances.
  • Seat cap: the seats of the team's plan, or 1 (the owner) without a live team subscription.
  • Credit packs cannot be bought from a team workspace.

Money

  • Amounts are always integer cents.
  • A refund rewrites the original order row (amount_refunded, status); it never adds a row.
  • Financial rows survive a user deletion (SET NULL). Organization deletion is refused once there is billing history (see Auth and database for the exact foreign keys).

The docs in the repository are written for agents and go deeper than these pages:

  1. CLAUDE.md at the root: the directory map and the rules above.
  2. The module's own CLAUDE.md, for example lib/billing/CLAUDE.md, lib/credits/CLAUDE.md, lib/auth/CLAUDE.md, lib/mail/CLAUDE.md, components/pricing/CLAUDE.md, actions/*/CLAUDE.md. Each one lists every file in the folder with one line on what it does, plus the folder's design rules.
  3. The file header. Most source files start with a comment block: [INPUT] (what it depends on), [OUTPUT] (what it exports), [POS] (its role), [PROTOCOL] (keep this header current). Read it before the code.
  4. skills/reference/: one file per area (payments.md, auth.md, database.md, email.md, cms.md, storage.md, ai.md, i18n.md and others). skills/reference/README.md lists them.
  5. .env.example: every setting, with a comment on what it is for. The same list is on Environment variables.

Docs move with the code

The repository treats its docs as a map of the code and keeps the two in step. There are three levels:

Level Where Updated when
L1 CLAUDE.md at the root a top-level module is added or removed, or an architecture rule changes
L2 <module>/CLAUDE.md a file in the module is added, removed or renamed, or its interface changes
L3 the header comment of each file its dependencies, exports or role change

The rule for anyone changing code, human or agent: change the code, then update the file header, then the module's CLAUDE.md, then the root CLAUDE.md if the change reaches that far. AGENTS.md repeats it: "Keep CLAUDE.md files and file headers in step with the code you change." A change that leaves the map wrong is not finished.

Before calling a code change done, run the type check and the tests:

pnpm lint   # npx tsc --noEmit; the project has no ESLint
pnpm test