Skip to content

feat: SaaS tier conversion — subscription schema, course gating, Zod validation#18

Merged
RahRha-v3-2 merged 1 commit into
mainfrom
claude/guap-saas-conversion-OEKXt
May 25, 2026
Merged

feat: SaaS tier conversion — subscription schema, course gating, Zod validation#18
RahRha-v3-2 merged 1 commit into
mainfrom
claude/guap-saas-conversion-OEKXt

Conversation

@RahRha-v3-2
Copy link
Copy Markdown
Owner

Database

  • 003_saas_tiers.sql: adds subscription_tier enum, subscription_status_val enum,
    tier/subscription_status/stripe_customer_id/current_period_end columns to users,
    tier_required to courses, is_premium to sections, subscription_events audit table,
    and record_tier_change() PL/pgSQL helper. All DDL is idempotent.

Type system

  • UserPayload gains tier field; AccessTokenPayload gains tier; UserRecord gains
    tier, subscriptionStatus, stripeCustomerId, currentPeriodEnd.
  • Single TIER_ORDER map in courseStore is the authority for tier comparisons.

Course ingestion & validation

  • courseStore.ts: all types now derived from Zod schemas (QuestionSchema, LessonSchema,
    SectionSchema, CourseSchema, CourseCatalogSchema). JSON is validated at boot;
    invalid catalogs fall back to the embedded course rather than crashing.
  • filterCourseForTier() strips content/questions from locked sections and adds
    locked: true flags so the frontend can render upgrade CTAs without extra fetches.
  • summarizeCourse() includes tierRequired for the catalog list view.

Auth

  • Access tokens now carry tier; /auth/refresh re-reads the DB row so a tier upgrade
    is reflected in the next refreshed token without requiring re-login.
  • /auth/me returns tier, subscriptionStatus, currentPeriodEnd.

New routes

  • GET /api/v1/subscription — caller's subscription state
  • GET /api/v1/subscription/history — audit log (empty in demo mode)
  • POST /api/v1/subscription/admin/set-tier — admin-only manual tier override
  • POST /api/v1/subscription/cancel — user self-cancel (400 for free tier)

Course gating

  • GET /api/v1/courses/:id returns full course with locked sections for insufficient tier.
  • GET /api/v1/courses/:id/lessons/:lessonId returns 403 TIER_REQUIRED for gated lessons.

New middleware

  • requireTier(minimum) enforces tier hierarchy; returns 403 with code TIER_REQUIRED.

Nginx

  • krai.conf updated for Vite SPA deployment: /assets/ cached immutably,
    static assets cached 30d, index.html never cached, SPA fallback via try_files.

Tests

  • All 63 tests pass (35 existing + 16 new tier integration tests + 1 updated token unit test).
  • New tiers.test.ts covers: catalog tier fields, free lesson access, subscription
    endpoint auth, admin gating, cancellation guard, updateUserTier memory fallback.

https://claude.ai/code/session_01CZmGEyfrJ6CQE4EE6CiLMJ

…validation

## Database
- 003_saas_tiers.sql: adds subscription_tier enum, subscription_status_val enum,
  tier/subscription_status/stripe_customer_id/current_period_end columns to users,
  tier_required to courses, is_premium to sections, subscription_events audit table,
  and record_tier_change() PL/pgSQL helper. All DDL is idempotent.

## Type system
- UserPayload gains `tier` field; AccessTokenPayload gains `tier`; UserRecord gains
  tier, subscriptionStatus, stripeCustomerId, currentPeriodEnd.
- Single TIER_ORDER map in courseStore is the authority for tier comparisons.

## Course ingestion & validation
- courseStore.ts: all types now derived from Zod schemas (QuestionSchema, LessonSchema,
  SectionSchema, CourseSchema, CourseCatalogSchema). JSON is validated at boot;
  invalid catalogs fall back to the embedded course rather than crashing.
- filterCourseForTier() strips content/questions from locked sections and adds
  `locked: true` flags so the frontend can render upgrade CTAs without extra fetches.
- summarizeCourse() includes tierRequired for the catalog list view.

## Auth
- Access tokens now carry `tier`; /auth/refresh re-reads the DB row so a tier upgrade
  is reflected in the next refreshed token without requiring re-login.
- /auth/me returns tier, subscriptionStatus, currentPeriodEnd.

## New routes
- GET  /api/v1/subscription         — caller's subscription state
- GET  /api/v1/subscription/history — audit log (empty in demo mode)
- POST /api/v1/subscription/admin/set-tier — admin-only manual tier override
- POST /api/v1/subscription/cancel  — user self-cancel (400 for free tier)

## Course gating
- GET /api/v1/courses/:id returns full course with locked sections for insufficient tier.
- GET /api/v1/courses/:id/lessons/:lessonId returns 403 TIER_REQUIRED for gated lessons.

## New middleware
- requireTier(minimum) enforces tier hierarchy; returns 403 with code TIER_REQUIRED.

## Nginx
- krai.conf updated for Vite SPA deployment: /assets/ cached immutably,
  static assets cached 30d, index.html never cached, SPA fallback via try_files.

## Tests
- All 63 tests pass (35 existing + 16 new tier integration tests + 1 updated token unit test).
- New tiers.test.ts covers: catalog tier fields, free lesson access, subscription
  endpoint auth, admin gating, cancellation guard, updateUserTier memory fallback.

https://claude.ai/code/session_01CZmGEyfrJ6CQE4EE6CiLMJ
@RahRha-v3-2 RahRha-v3-2 merged commit 313ec94 into main May 25, 2026
3 of 4 checks passed
@RahRha-v3-2 RahRha-v3-2 deleted the claude/guap-saas-conversion-OEKXt branch May 25, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants