feat: SaaS tier conversion — subscription schema, course gating, Zod validation#18
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Database
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
tierfield; AccessTokenPayload gainstier; UserRecord gainstier, subscriptionStatus, stripeCustomerId, currentPeriodEnd.
Course ingestion & validation
SectionSchema, CourseSchema, CourseCatalogSchema). JSON is validated at boot;
invalid catalogs fall back to the embedded course rather than crashing.
locked: trueflags so the frontend can render upgrade CTAs without extra fetches.Auth
tier; /auth/refresh re-reads the DB row so a tier upgradeis reflected in the next refreshed token without requiring re-login.
New routes
Course gating
New middleware
Nginx
static assets cached 30d, index.html never cached, SPA fallback via try_files.
Tests
endpoint auth, admin gating, cancellation guard, updateUserTier memory fallback.
https://claude.ai/code/session_01CZmGEyfrJ6CQE4EE6CiLMJ