An async FastAPI application that handles authentication and Stripe-backed subscription billing. It uses SQLAlchemy for data access, Stripe Checkout for payments, and Celery (with Redis) for background jobs such as subscription emails and expiry sweeps.
- Entry point:
src/main.pywires routers, CORS, slowapi rate limiting, request logging, and validation error handling. - Routing:
src/auth/router.pyandsrc/billing/router.pyexpose the API surface. Dependencies inject repositories/services and enforce auth/admin checks. - Services: Business rules live in
src/auth/service.pyandsrc/billing/service.py. - Repositories: Data access layer (
src/repository.py,src/auth/repository.py,src/billing/repository.py) built on async SQLAlchemy sessions. - Data models:
src/auth/models.py,src/billing/models.py,src/models.pydefine tables for users, profiles, plans, subscriptions, payments, refresh tokens, and OTP codes. - Utilities: JWT handling (
src/jwt.py), hashing (src/hashing.py), email config (src/utils.py), rate limiting (src/rate_limiter.py), logging (src/logging.py), and OAuth/OTP helpers (src/auth/utils.py). - Background work: Celery worker/beat in
src/celery_app.pywith tasks insrc/billing/tasks.pyand placeholdersrc/tasks.py. - Templates: Jinja email templates under
templates/email/for verification, reset, OTP, and subscription emails. - Infrastructure: Docker Compose for API, Postgres, Redis, Celery worker/beat, and pgAdmin.
- FastAPI receives the request, rate limiting runs via slowapi, and CORS headers are applied.
- Router-level dependencies resolve repositories and current user/admin guards (
src/auth_bearer.py). - Service methods apply business rules and call repositories to read/write Postgres.
- Token/cookie handling occurs at the router layer for login/refresh/OAuth flows.
- Responses return Pydantic schemas; validation errors use a custom handler returning
{errors: {field: message}}.
- Registration & Profiles:
UserService.register_uservalidates unique email/username, hashes passwords, and auto-creates an emptyProfileviaUserRepository.create. - Login (password): Verifies Argon2 hash, issues access/refresh JWTs (
src/jwt.py), stores hashed refresh token with JTI inrefresh_tokens(rotation enforced), and sets httpOnly cookie. - Refresh rotation:
/refresh-tokenverifies JWT, looks up JTI in DB, validates non-revoked/non-expired, issues new tokens, revokes old, and stores new hashed refresh token. - Email verification: Validation token generated by
validation_secret_key;/verifymarksis_verified;/request/verifyresends via BackgroundTasks. - Password reset/change: Reset token emailed via
send_password_reset_email;/new-passwordupdates hash;/change-passwordchecks old password for authenticated users. - OTP login:
/request/login-codecreates hashed code (LoginCodeRepository), emails it;/login/codeverifies against latest, enforces expiry, deletes on use, and issues tokens. - OAuth (Google/GitHub): State cookie set on login endpoints; callback exchanges code for token (
auth/utils.py), fetches profile/email, auto-provisions user with provider flag, and issues tokens. - Account deactivation:
/deactivatemarksis_active=False; further access is blocked by auth dependency checks. - Access control:
auth_bearer.pyenforces active/verified users and admin-only routes for plan management.
- Plans: CRUD via
PlanService/PlanRepository; plan tiers (PlanTier) stored on plans for feature gating; soft delete setsis_active=False. Stripe product/price is created/updated viaStripeGatewayand stored on the plan. - Subscriptions:
SubscriptionRepoistorytracks access windows (current_period_end). New subscriptions startPAST_DUEuntil webhook confirmation. Duplicate active subs are blocked. - Checkout & Upgrade:
/billing/subscriptions/subscribeand/upgradecreate Stripe Checkout sessions with metadata (plan/user and optionalupgrade_from_subscription_id). Customer is ensured/created before checkout. - Cancellation:
/billing/subscriptions/cancelmarkscancel_at_period_endand, for Stripe, modifies the subscription. Local record updated withcanceled_at/period end. - Payments:
PaymentRepositorystores invoices (provider invoice id, amount, currency, status). Recorded oninvoice.payment_succeededwebhooks.
- Webhook endpoint
/billing/stripe/webhookverifies signature viastripe.Webhook.construct_event. - Events handled:
checkout.session.completed: fetches Stripe subscription, handles upgrade (cancels old), and creates a local subscription.invoice.payment_succeeded: retrieves subscription, updates period start/end, records payment, and dispatches Celery email tasks (create vs renewal based onbilling_reason).customer.subscription.deleted: cancels local subscription, settingcurrent_period_endto now.invoice.payment_failed: marks subscriptionPAST_DUE.
- Stripe metadata carries
plan_id,plan_code,user_id, and upgrade source IDs for proper reconciliation.
- Celery worker (
src.celery_app.celery_app) and beat (src.celery_app.beat_app) use Redis URLs from env. - Tasks:
send_subscription_email_taskandsend_update_subscription_email_taskdispatch templated emails;expire_subscriptions_task(beat) cancels subscriptions whosecurrent_period_endhas passed. - Legacy placeholder
src/tasks.py:expire_subscriptionsprints a TODO and is unused by beat. - Sync DB engine (
SYNC_DATABASE_URL) is used inside Celery tasks viaget_sync_session.
- users: id, email, username, password (nullable for social), admin/active/verified flags, provider, stripe_customer_id, timestamps; 1-1 profile, 1-many subscriptions and refresh tokens.
- profiles: per-user names/image, timestamps; created alongside user.
- login_codes: hashed OTPs with expiry per user.
- refresh_tokens: hashed token with JTI, expiry, revoked flags, replacement JTI.
- plans: name/code, price_cents, currency, billing_period, tier, is_active, Stripe product/price IDs, timestamps.
- subscriptions: user/plan FKs, status, provider/provider IDs, period start/end, cancel flags/timestamps.
- payments: subscription/user FKs, provider invoice id, amount/currency, status, provider enum; unique per provider/invoice id.
- Auth: registration/login/refresh, email verification (request/verify), password reset/change, OTP login, Google/GitHub OAuth, deactivate account.
- Billing: plans list/create/get/update/delete (admin for mutations); subscriptions me/subscribe/upgrade/cancel; payments me; Stripe webhook (rate-limit exempt).
- Rate limiting: default 5/min per Authorization header or IP (
src/rate_limiter.py).
- Validation errors use
validation_exception_handlerto return{"errors": {field: message}}with 422 status. - Business rule violations raise
HTTPExceptionwith descriptive messages and appropriate HTTP codes. - Stripe webhooks return error strings on signature/validation failures; otherwise return
True.
- Passwords and OTP codes hashed with Argon2 (
src/hashing.py). - JWTs include
jti,exp,iat; refresh tokens stored hashed in DB and rotated on every refresh/login. - Cookies for refresh tokens are httpOnly,
samesite="lax"; setsecure=Truein production. - OAuth state cookies defend against CSRF on social callbacks.
- Rate limiting is enabled globally; webhook route is exempt.
- CORS is wide open by default; restrict origins/headers/methods for production.
- Settings loaded by
pydantic-settings(src/config.py) from.env. - Provide distinct URLs for async API DB, sync Celery DB, and tests.
- Logging configured via
src/logging.pyto stdout andlogs/app.logwith rotation.
- Install deps with
uv syncand create.env(see README). - Run
uv run alembic upgrade headto create tables/seeds (plan seed migration exists underalembic/versions). - Start services:
uv run uvicorn src.main:app --reload, Celery worker/beat, Postgres, and Redis. - Develop features in service/repository layers; add/extend Pydantic schemas and tests.
- Run
uv run pytestbefore merging; mock Stripe/email where needed.
- Pytest via
uv run pytestwithpytest-asyncio, httpxAsyncClient+ASGITransporthitting the FastAPI app directly. - DB setup/teardown uses
TEST_DATABASE_URLand creates/drops schema per session (tests/conftest.py). - Rate limiter disabled in tests; Stripe and email interactions are monkeypatched in billing/auth tests.
ProfileReposiotry.get_by_user_idandProfileServiceare stubs.src/tasks.py:expire_subscriptionsis unimplemented (beat usesbilling.tasks.expire_subscriptions_taskinstead).- Placeholder modules for constants/config/exceptions exist under
src/authandsrc/billing. - Enum options
PAYMOBandPaymentStatusstates are defined but not fully wired.