feat: multi-tenant shared-schema — SubscriptionPlan, org scope, visibility, AST storage, data retention consent#1588
Conversation
- Add SubscriptionPlan model with capability/limit fields - Update User model: planId FK, new relations (filterSources, configurations, filterListAsts, dataRetentionConsents), index - Replace Organization model: tier/planId/retentionDays fields, full back-relations - Update Member model: tierOverride field - Replace FilterSource: org scope, visibility, isFeatured, composite unique (url,ownerUserId,organizationId) - Update CompiledOutput: organizationId, visibility, org relation + indexes - Add Configuration model: org/user ownership, visibility, fork/star social graph - Add FilterListAst model: AST storage gated by plan - Add DataRetentionConsent model: append-only compliance audit log - Add migration: prisma/migrations/20260414000000_multi_tenant_shared_schema/migration.sql - Add docs: MULTI_TENANCY.md, multi-tenancy-schema.md, auth/multi-tenancy.md, admin/subscription-plans.md - Update docs/SUMMARY.md with new entries Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/f11d4453-d2f5-4190-8d55-083c7c469b5c Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
…note Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/f11d4453-d2f5-4190-8d55-083c7c469b5c Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
🗄️ Database Migration Plan
📂 Changed Migration FilesPrisma (
☁️ Cloudflare D1Main Database (
|
|
@copilot Fix CI |
…lation does not exist) The organization and member tables were defined in Prisma schema but never created by any existing migration. The migration was trying to ALTER TABLE on a non-existent organization table, causing P3018/42P01. Fix: Add CREATE TABLE IF NOT EXISTS for both tables (with all base + new columns) at the top of the migration, before any ALTER TABLE or FK references. ALTER TABLE IF NOT EXISTS guards are retained for idempotency on environments where the tables already exist. Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/b80c1f48-2545-471e-9aa5-56290e9e9e50 Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Fixed in commit Root cause: the Fix: the migration now |
@copilot Same step is still broken, please fix this. |
…backfill, consent FK, comment fix, publicOnly test Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/b973dd1d-16b5-445e-9d77-f08fb3b8def3 Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Applied in commit
|
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
@copilot CI is still broken. Please fix it (this is now the 5th time) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 15 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
.github/workflows/neon-branch-create.yml:212
- This step uses
set +eto capture the non-zero exit code fromprisma migrate status, but it never re-enables-e(or otherwise checks exit codes) before runningprisma migrate resolve. Ifmigrate resolvefails (bad migration name, connectivity issue, auth), the script can continue and report success, making CI flaky/harder to debug. Suggest scoping the non-fail behavior to the status command only (e.g.,STATUS=$(... || true)) and then restoringset -euo pipefailbefore the grep/resolve loop.
run: |
set +e
STATUS=$(npx prisma migrate status 2>&1)
set -e
|
@copilot apply changes based on the comments in this thread |
…d v4 strict UUID validation) Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/2fbecbe1-5b7c-4424-a253-454cd4c83242 Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Fixed in commit Root cause: Zod v4 ( Fix: replaced the invalid hardcoded UUID with the |
…version docs, FilterSource CASCADE, DataRetentionConsent no-FK Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/38bb1956-51ee-4efb-9791-b7fa02ca7267 Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Applied in commit
Fixed in commit |
|
@copilot apply changes based on the comments in this thread |
- Run prisma generate to rebuild prisma/generated/ to match schema.prisma: new models SubscriptionPlan, Configuration, FilterListAst, DataRetentionConsent; updated FilterSource (visibility/organizationId), Organization, User, CompiledOutput, Member; updated internal/class.ts inlineSchema+DMMF; updated prismaNamespace enums and type payloads - Fix @/prisma/generated → ../../prisma/generated/client.ts in docs/auth/multi-tenancy.md (@ alias maps to ./src/ per deno.json) Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/2b214751-c9be-493c-8866-dda3f2f46b73 Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Applied in commit
|
Description
Implements the multi-tenant shared-schema architecture: all orgs/users share tables discriminated by
organizationId+visibilityenum. Adds plan-gated feature flags, per-member tier overrides, and an append-only data retention consent audit log. Also fixes three layered CI issues: a migration SQL ordering bug (P3018), a workflow grep pattern that never matchedprisma migrate statusoutput, and a broken Neon branch restore API call that was replaced withprisma migrate resolve --rolled-back. Addresses all PR review comments to improve schema correctness and consistency. Fixes a CI/Test gate failure caused by generated Prisma client types not being updated after theisPublic→visibilityschema change. Further addresses a second round of review feedback: ownership invariants onCreateFilterSourceSchema,organizationIdplumbing in the storage adapter, migration data correctness (visibility backfill, consent FK lifecycle, misleading comment removal), and additional unit test coverage forlistFilterSources(publicOnly=true). Fixes a test-level CI failure caused by a hardcoded non-RFC-4122 UUID being rejected by Zod v4's strict UUID validation. Addresses a third round of review feedback: DB CHECK constraints forvisibilitycolumns,FilterSource.ownerUsercascade semantics,DataRetentionConsentFK lifecycle compatibility with hard-delete, andparser_versionsemver documentation. Addresses a fourth round of review feedback: fixes incorrect@/prisma/generatedimport path in docs and fully regenerates allprisma/generated/client artifacts to match the updated schema.Changes
prisma/schema.prismaSubscriptionPlanmodel — authoritative source for tier capabilities (rateLimitPerMinute/Day,maxFilterSources,maxCompiledOutputs,astStorageEnabled,translationEnabled,globalSharingEnabled,batchApiEnabled,retentionDays,isOrgOnly). BothUserandOrganizationFK to it;tierfields remain as denormalized hot-path cache.User— addedplanIdFK, back-relations toFilterSource,Configuration,FilterListAst; removed back-relation toDataRetentionConsent(no FK on consent table)Organization— replaced: addedtier,planId,retentionDays,retentionPolicyAcceptedAt, full back-relations to resource models; removed back-relation toDataRetentionConsent(no FK on consent table)Member— addedtierOverride String?for per-member tier restriction within an orgFilterSource— replaced: removed global@unique url; removedisFeaturedboolean in favour ofvisibility='featured'as the single source of truth for featured/pinned sources; addedorganizationId,visibility; composite unique(url, ownerUserId, organizationId); changedownerUserrelation fromonDelete: SetNulltoonDelete: Cascadeto prevent partial-unique index conflicts when a user is deletedCompiledOutput— addedorganizationId,visibility, org relation + indexesConfigurationmodel — supersedesUserConfiguration(retained as legacy); adds org/public visibility, self-referential fork graph (forkedFromId),starCount/forkCountfor public rankingFilterListAstmodel — AST storage withparserVersionfor staleness detection; gated bySubscriptionPlan.astStorageEnabled(available to pro+ tiers: pro, vendor, and enterprise)DataRetentionConsentmodel — append-only compliance audit log; trackspolicyVersion,retentionDays,dataCategoriesper user or org;userId/organizationIdare plain UUID columns with no FK constraints (preserves audit trail through hard-delete of users/orgs); DB CHECK constraint enforces exactly one of the two columns is non-null per rowMigration
prisma/migrations/20260414000000_multi_tenant_shared_schema/migration.sql— idempotent (IF NOT EXISTS), creates theorganizationandmembertables before anyALTER TABLEor FK references, handles thefilter_sources_url_key→ composite unique transition, seeds four base plans (free,pro,vendor,enterprise).CREATE TABLE IF NOT EXISTS "organization"and"member"before anyALTER TABLEor FK references.filter_sourcesFK + index: FK constraintfilter_sources_owner_user_id_fkeychanged toON DELETE CASCADE(wasSET NULL); prevents partial-unique constraint violations on user delete.filter_sourcespartial unique index:filter_sources_url_global_unique—UNIQUE(url) WHERE owner_user_id IS NULL AND organization_id IS NULL.filter_sourcesvisibility backfill:UPDATE filter_sources SET visibility = CASE WHEN is_public THEN 'public' ELSE 'private' END WHERE visibility IS NULLbefore enforcing NOT NULL.visibilityCHECK constraints: addedCHECK (visibility IN ('private','org','public','featured'))tofilter_sources,compiled_outputs,configurations, andfilter_list_aststo enforce the closed set at the DB level.data_retention_consentsno FK:user_idandorganization_idare stored as plain UUID columns withoutREFERENCESconstraints; idempotentDROP CONSTRAINTblocks remove any previously created FKs on re-run. Preserves immutable audit log through hard-deletes of users/orgs.data_retention_consentsCHECK constraint:CHECK ((user_id IS NOT NULL) <> (organization_id IS NOT NULL))enforces exactly one owner per consent row.Storage layer
src/storage/schemas.ts: replacedisPublic: z.boolean()withvisibility: z.enum(['private', 'org', 'public', 'featured']); added optionalorganizationId; refinements enforceorgvisibility requiresorganizationIdandownerUserId/organizationIdare mutually exclusive.src/storage/HyperdriveStorageAdapter.ts: updatedcreateFilterSourceto plumborganizationId; updatedlistFilterSourcesto filter onvisibility IN ('public', 'featured')whenpublicOnly=true.src/storage/schemas.test.ts: added tests fororgvisibility requiringorganizationId, and for theownerUserId/organizationIdmutual-exclusion refinement.src/storage/HyperdriveStorageAdapter.test.ts: updated mockfindManyto handle the{ in: [...] }filter shape; addedlistFilterSources(true)test asserting onlypublic/featuredrows are returned; replaced non-RFC-4122 hardcoded UUID with validTEST_UUID_1constant.Generated Prisma client types
Fully regenerated all
prisma/generated/artifacts to matchschema.prisma:prisma/generated/models/FilterSource.ts: updated allisPublictype entries tovisibility; addedorganizationIdto all relevant input/output/filter types.prisma/generated/models/SubscriptionPlan.ts(new): complete CRUD input/output/filter types for the new model.prisma/generated/models/Configuration.ts(new): complete CRUD input/output/filter types for the new model.prisma/generated/models/FilterListAst.ts(new): complete CRUD input/output/filter types for the new model.prisma/generated/models/DataRetentionConsent.ts(new): complete CRUD input/output/filter types for the new model.prisma/generated/models.ts: barrel updated to export all new model types.prisma/generated/internal/class.ts:inlineSchemaand DMMF updated to reflect the full new schema (new models,visibility/organizationIdfields, noisPublic, no FK relations onDataRetentionConsent).prisma/generated/internal/prismaNamespace.tsandprismaNamespaceBrowser.ts: updatedFilterSourceScalarFieldEnumisPublic→visibility; addedorganizationIdenum member; added enum members for all new models.Workflow fix
.github/workflows/neon-branch-create.yml— stuck P3009 migrations resolved viaprisma migrate resolve --rolled-back <name>; detection pattern narrowed togrep -qE "P3009|Following migration.*have failed".Documentation
docs/architecture/MULTI_TENANCY.md— architecture decision, ownership model, visibility semantics, tier resolution, rate limitingdocs/database-setup/multi-tenancy-schema.md— ERD, model deep-dives, index strategy; updatedparser_versionstaleness section to warn that lexicographic<comparison is unreliable for semver and recommends equality checks or a numericschemaVersioncolumn insteaddocs/auth/multi-tenancy.md— tier resolution flowchart, feature flag enforcement,DataRetentionConsentat signup/org creation; fixed incorrect@/prisma/generatedimport path to../../prisma/generated/client.ts(the@/alias maps to./src/perdeno.json)docs/admin/subscription-plans.md— SQL snippets for plan assignment, member overrides, re-seedingTesting
Zero Trust Architecture Checklist
This PR touches
prisma/,src/storage/,docs/, and.github/workflows/— noworker/orfrontend/handler changes. ZTA checklist N/A.Worker / Backend
*) on write/authenticated endpoints — N/A[vars]) — N/A.prepare().bind()(no string interpolation) — N/AFrontend / Angular
CanActivateFnauth guards — N/AlocalStorage) — N/AAPI Shield / Vulnerability Scanner
operationIdinopenapi.yaml— N/Asecurity:annotation — N/A404— N/Acloudflare-schema.yamlregenerated ifopenapi.yamlchanged — N/AOriginal prompt
Overview
Based on a detailed architecture discussion, implement the multi-tenant shared-schema changes to
prisma/schema.prismaand create corresponding documentation in/docs. The architecture decision is: multi-tenant, shared schema (all orgs in the same tables discriminated byorganizationId+ avisibilityenum), with a future pathway for enterprise-tier DB-level isolation via Neon branching.Schema Changes (
prisma/schema.prisma)Apply ALL of the following changes to the existing schema. Preserve all existing models and comments unless explicitly told to replace them. The goal is additive migration — no existing columns are dropped (only new ones added, or existing models expanded).
1. Add new
SubscriptionPlanmodelInsert this model before the
Usermodel (after the generator/datasource block):2. Update
UsermodelAdd the following fields to the
Usermodel:Also add
@@index([planId])to theUsermodel.3. Update
Organizationmodel — replace the existingOrganizationmodel entirely4. Update
Membermodel — addtierOverrideAdd the following field to the existing
Membermodel: