## Why Stripe checkout used Stripe-only tables. This extracts a shared payment CORE. New checkout writes `payment_order`. Old Stripe tables stay so in-progress Sessions can still settle. This PR is [1/2]. [#2368](https://github.com/moeru-ai/airi/pull/2368) is [2/2]. That PR archives leftover Stripe tables after in-progress Sessions finish or expire. ## Changes - Add `payment_order` and `provider_account`. - Copy `stripe_checkout_session` into `payment_order`. - Copy `stripe_customer` into `provider_account`. - Keep `stripe_*` tables and `user_flux.stripe_customer_id`. - New checkout writes `payment_order` and stores `metadata.payment_order_id`. - Webhook resolves new Sessions by `metadata.payment_order_id`. - Webhook resolves older Sessions by `provider_order_id`, then by a leftover `stripe_checkout_session` row. - That leftover-row lookup covers Sessions opened before this deploy, and rows written while 0023 runs. [#2368](https://github.com/moeru-ai/airi/pull/2368) deletes it after those Sessions finish or expire. ## Test plan - [x] `pnpm exec vitest run server/apps/api/src/services/domain/payment/tests/payment.test.ts server/apps/api/src/routes/stripe` - [x] `pnpm -F @proj-airi/api-server typecheck` - [x] `git diff --check` ## Visual changes No user-visible changes. ## Open: settle after account deletion Account deletion stamps `payment_order.deletedAt`. A Checkout Session that is still open can still pay after that stamp. This PR keeps `main` behavior. `settle` loads the row by id, including a soft-deleted row, then marks it `paid` and credits Flux. Follow-up policy: if `deletedAt` is set, skip. Do not update the archive row. Do not credit Flux. Do not insert a live `provider_account`. Stripe remains the payment record. Skip matches the soft-delete rule: a deleted row is gone, not write it again. Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: RainbowBird <git@luoling.moe>
2.7 KiB
@proj-airi/api-server
Project AIRI's resource API. Authentication is a separate workspace app at
server/apps/auth; this package does not instantiate Better Auth or expose
auth/OIDC routes.
Responsibilities
- Hono business APIs and WebSocket endpoints.
- Characters, chats, providers, Flux, Stripe, model routing, and billing.
- PostgreSQL migration ownership for the currently shared database. Drizzle reads the checked-in
drizzle/journal and SQL files at startup. - Redis cache, configuration KV, and cross-instance Pub/Sub.
- Local verification of Auth-issued OIDC JWTs through public JWKS.
Payment
src/services/domain/payment owns pack grant and payment_order rows.
CORE exposes openPending, bindProcessorOrder, abandon, settle,
and deleteAllForUser. Checkout and package list live in the Stripe
adapter on /api/v1/stripe/*. CORE never sees a raw processor event.
The adapter maps the processor result onto a ClaimReceipt, then calls
settle.
Run locally
pnpm -F @proj-airi/api-server dev
pnpm -F @proj-airi/api-server typecheck
pnpm -F @proj-airi/api-server exec vitest run
pnpm -F @proj-airi/api-server build
Run the complete local backend from the repository root:
pnpm dev:backend
For source-level debugging, start @proj-airi/api-server and
@proj-airi/auth-server separately instead.
server/docker-compose.yaml exposes the local Caddy gateway at http://localhost:6112 and keeps
the API and Auth container ports private.
Service boundaries
AUTH_SERVER_URLis Auth's canonical public issuer origin used for JWKS, issuer, and audience validation. It must exactly equal Auth'sPUBLIC_URL./internal/auth/*is reachable only on the deployment's trusted private network. The public edge must reject/internal/*and the API service must not have its own public ingress.AUTH_SERVER_INTERNAL_URLoptionally sends JWKS fetches directly to Auth on the private network while issuer and audience remainAUTH_SERVER_URL.- Auth tables and principal types come from
@proj-airi/auth-shared; no module underserver/apps/authis imported.
Railway
Deploy this as the Resource API Railway service with Config File Path
/server/apps/api/railway.toml; keep the service Root Directory at the
repository root because the Dockerfile copies shared workspace packages. The
config owns its Dockerfile, start command, /readyz healthcheck, and the
watch patterns for every copied build input.
Set AUTH_SERVER_INTERNAL_URL from Auth's Railway private domain. It is only
the private JWKS route; AUTH_SERVER_URL remains the public Auth issuer URL.
See server/README.md for the complete
cross-service variable and migration contract.