gitea-saas-core/ARCHITECTURE.md
2026-08-25 04:02:11 -05:00

2.9 KiB

gitea-saas-core — Monetization Core

Two products, one billing core. The lane system (issue -> branch -> PR -> review -> merge) is the delivery machine; this service is the client-facing moat around it.

Roles

  • Billing core (shared): clients, ledger, holds, Stripe top-ups, credits. Pure Python + SQLite. No knowledge of lanes or Gitea.
  • Factory front-end (option 1): client repo provisioning, issue intake with acceptance criteria, unfunded gating, delivery receipts on merge.
  • Eval farm front-end (option 2): batch task intake -> internal eval-task issues, results table, emitter (webhook or file drop).

Billing invariants (fail-closed)

  • balance = credits - debits - active holds (never negative)
  • every mutation is idempotent via an idempotency_key (Stripe event id, intake uuid, etc.)
  • holds expire (default 30 min) and release back automatically
  • debit happens exactly once per delivered issue (key = gitea repo + issue number + merge sha)
  • client with balance below the debit floor is unfunded: the dispatcher must never claim its issues (gate = unfunded label skipped by the lane survey)

Gating model (no dispatcher state)

The dispatcher scans all repos under the owner. Intake creates issues carrying the unfunded label; payment removes it and applies the tier's priority label. The lane survey already skips a known label set — unfunded joins it. Zero-balance re-locks by re-adding unfunded on the next delivery check. No new dispatcher machinery, no shared DB reads.

Layout (target)

gitea_saas/
  billing_core.py      # ledger schema + operations (pure, no HTTP)
  server.py            # Flask app (billing API + dashboards)
  stripe_client.py     # Stripe adapter (mockable seam, env keys)
  gitea_client.py      # Gitea API client (mockable seam, env token)
  intake_factory.py    # option 1: provision client repo + issues + unlock
  intake_eval.py       # option 2: eval batches -> issues + results + emitter
  delivery.py          # merge event -> receipt debit + comment + relock check
  config.py            # tier config, rates (loaded from env/config, never hardcoded)
  schema.sql / db.py   # SQLite migration
tests/                 # pytest, hermetic (mock Stripe + Gitea, tmp SQLite)
dashboard/             # html/static if needed (mirror botmanager pattern)

Environment (provided by operator at deploy time, mocked in CI)

  • STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET (optional: absent -> mock mode)
  • GITEA_BASE_URL / GITEA_TOKEN (factory/eval front-ends)
  • SAAS_DB_PATH, SAAS_API_KEY (X-API-Key auth), SAAS_PORT

Tier/rate config (operator sets later; centralize in config.py)

Enterprise default: priority p1, auto-merge on green (opt-in per client), rate per issue. Rates are config, not code. First client onboarding is manual; self-serve (Stripe checkout -> auto-provision) is the stated next step after core + both front-ends land.