## 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>
AIRI Backend
Project AIRI's hosted backend source lives under this folder. Workspace package
names stay stable; the directory groups service source and database ownership
while production deployment configuration remains in proj-airi/airi-railway.
Layout
apps/api: resource API, business domains, database migrations, and API runtime.apps/auth: standalone Better Auth and OIDC service.packages/auth-shared: Auth-owned database schema and principal contracts.packages/server-sdk-shared: Eventa contracts for the hosted chat WebSocket.dev/caddy: local-only public edge routing for the shared Auth/API origin.docker-compose.yaml: complete local API + Auth + PostgreSQL + Redis + Caddy stack.
Run locally
From the repository root:
pnpm dev:backend
The command uses server/docker-compose.yaml and exposes only Caddy at
http://localhost:6112.
Railway deployment
API and Auth are separate long-running Railway services built from the same repository. Keep each service's Root Directory at the repository root: both Dockerfiles copy workspace manifests and shared packages from that build context. In each Railway service, configure the Config File Path explicitly:
| Service | Config File Path | Public role | Private dependency |
|---|---|---|---|
| Resource API | /server/apps/api/railway.toml |
Product and resource API | Auth issuer and JWKS |
| Auth | /server/apps/auth/railway.toml |
Better Auth and OIDC issuer | Resource API deletion endpoint |
Each config pins its own Dockerfile, start command, /readyz healthcheck, and
watch patterns. A change only deploys a service when it changes that service,
one of its copied shared packages, or a copied root build input.
Service-to-service contract
Share database, Redis, and observability variables using Railway reference variables rather than copying secret values between services. Configure the two directional private links as follows:
| Consumer | Variable | Value source | Purpose |
|---|---|---|---|
| Resource API | AUTH_SERVER_URL |
Auth's canonical public issuer URL | JWT issuer, audience, and public JWKS identity |
| Resource API | AUTH_SERVER_INTERNAL_URL |
Auth's Railway private domain | Private JWKS fetch; it does not change issuer validation |
| Auth | PUBLIC_URL |
Auth's canonical public issuer URL | Better Auth and OIDC issuer URL; must equal API AUTH_SERVER_URL |
| Auth | RESOURCE_SERVER_URL |
API's Railway private domain | Private call before deleting a user's business data |
Set RATE_LIMIT_TRUSTED_PROXY=railway only for services directly receiving
Railway proxy traffic. Keep /internal/* private: Auth calls the API over its
private domain, and public routing must not expose the API's internal Auth
routes.
The API remains the shared database migration owner. Do not add a Railway
pre-deploy migration command to Auth, and do not make Auth startup run shared
migrations. After either service deploys, Railway must receive 200 from that
service's /readyz; deployment success alone is not sufficient evidence that
the service can reach its required dependencies.
Package boundaries
Frontend applications remain under apps/. Hosted-backend packages that
define a resource API protocol can live under server/packages/, even when a
frontend consumes their generated contract.
Cross-runtime server SDK and protocol packages remain under packages/
because Web, Electron, plugins, and independent services consume them.
Production Caddy routing, OpenTelemetry Collector configuration, observability
storage, and Grafana dashboards live in proj-airi/airi-railway so deployment
topology is not duplicated in the application repository.