Captured: 2026-04-29T15:25:58Z
Branch: phase-2-rls-migration
Starting HEAD: 7cb07b9
This document materializes the read-only Phase 2.1 audit that preceded RLS implementation. It covers the current RLS artifacts in the repo, non-prod DB availability, live deployment visibility, and the risks in the current Prisma tenant-scoping shim.
- Local Postgres is available now:
pg_isreadyreturned/tmp:5432 - accepting connections
- Local Docker-based non-prod targets also exist:
- docker-compose.yml defines a Postgres container
proposal_engine_dbon5435 - docker-compose.pgbouncer.yml defines PgBouncer on
6432intransactionpool mode
- docker-compose.yml defines a Postgres container
- Repo-side non-prod deployment hints exist:
- cloudbuild.yaml references a staging deploy target
proposal-engine-staging - scripts/setup-cloud-sql.sh shows Cloud SQL socket-style DSNs for local setup
- cloudbuild.yaml references a staging deploy target
- Cloud SQL discovery across visible projects was blocked by CLI auth refresh failure:
ERROR: (gcloud.projects.list) There was a problem refreshing your current auth tokens:
Reauthentication failed. cannot prompt during non-interactive execution.
Please run:
$ gcloud auth login
- None verified from this audit pass because
gcloud projects listand follow-ongcloud sql instances listcould not complete non-interactively.
- cloudbuild.yaml sets
_ENVIRONMENT: 'staging'by default and deploysproposal-engine-stagingoutside production. - docker-compose.yml defines a local Postgres dev DB
proposal_engine. - docker-compose.pgbouncer.yml simulates the production pooled connection model locally.
.envfiles present locally:.env.example.env.local.env.production.example
- Native/local socket PostgreSQL: available on
/tmp:5432 - Docker Postgres: configured in compose, but not confirmed running from this pass
- Local PgBouncer: configured in compose, but not confirmed running from this pass
Use local Postgres plus local PgBouncer transaction-mode pooling as the first live validation target. That setup is available without IAM and directly exercises the highest-risk assumption in the current design: SET LOCAL app.current_tenant_id under pooled connections.
- The repo clearly targets Cloud Run:
- cloudbuild.yaml deploys
proposal-enginefor production andproposal-engine-stagingfor staging - deploy.sh deploys service
proposal-engine
- cloudbuild.yaml deploys
- Environment defaults show localhost in dev and Run-style URLs in production examples:
.env.example:NEXT_PUBLIC_APP_URL="http://localhost:3000".env.production.example:NEXT_PUBLIC_APP_URL="https://your-app.run.app"
- Git deployment markers exist:
- branch:
production-hardening-complete - commit history includes
Merge pull request #1 from danishxsethi/production-hardening-complete
- branch:
proposal-engine-dbis already known from Phase 0.5 to be stopped and dormant, so it should not be assumed to represent live ProposalOS traffic.
- Actual Cloud Run inventory was unverifiable during this pass because
gcloud run services listandgcloud run jobs listfailed with the same non-interactive reauth error:
ERROR: (gcloud.run.services.list) There was a problem refreshing your current auth tokens:
Reauthentication failed. cannot prompt during non-interactive execution.
Please run:
$ gcloud auth login
- No
.vercel/directory detected - No
vercel.json - No
netlify.toml - Current repo evidence points most strongly to GCP Cloud Run, not Vercel or Netlify
Live deployment configured, runtime status unverifiable from current CLI auth.
The repo has clear Cloud Run deployment machinery, but this audit pass could not confirm:
- whether any ProposalOS service is currently live,
- which Cloud SQL instance it attaches to,
- or which DB role the app uses (
postgres,app_user, or another role).
gcloud auth login
gcloud auth application-default login
for proj in $(gcloud projects list --format='value(projectId)'); do
echo "=== Project: $proj ==="
gcloud run services list --project="$proj" --format='table(metadata.name,status.url,status.latestReadyRevisionName)' | head -10
gcloud sql instances list --project="$proj" --format='table(name,region,state)' | head -10
done- prisma/enable_rls.sql
- prisma/migrations/rls/enable_rls.sql
- prisma/migrations/rls/revert_rls.sql
- prisma/schema.prisma
prisma/enable_rls.sql— 10,589 bytes — modified2026-04-25 02:24:17prisma/migrations/rls/enable_rls.sql— 20,190 bytes — modified2026-02-27 21:48:29prisma/migrations/rls/revert_rls.sql— 1,398 bytes — modified2026-02-25 14:22:23
prisma/enable_rls.sqlis tracked in gitprisma/migrations/**is ignored by .gitignore via/prisma/migrations/git ls-filesshows:- tracked:
prisma/enable_rls.sql,prisma/schema.prisma - not tracked: all files under
prisma/migrations/
- tracked:
The repo currently has two divergent RLS artifacts, and the structurally stronger one lives in a gitignored directory. That means there is no single authoritative tracked RLS source of truth today.
Using prisma/migrations/rls/enable_rls.sql as the stricter baseline:
35/67tenant-bearing models haveCREATE POLICY32/67are missing0haveCREATE POLICYwithoutENABLE ROW LEVEL SECURITY0haveCREATE POLICYwithoutFORCE ROW LEVEL SECURITY
Across both divergent files combined, nominal coverage reaches 46/67, but that number is not trustworthy because the tracked file contains @@map mismatches for physical table names.
| Model | Postgres table | tenantId column present | CREATE POLICY present | ENABLE ROW LEVEL SECURITY present | FORCE ROW LEVEL SECURITY present |
|---|---|---|---|---|---|
| Audit | Audit | YES | YES | YES | YES |
| Finding | Finding | YES | YES | YES | YES |
| Proposal | Proposal | YES | YES | YES | YES |
| ProposalAcceptance | ProposalAcceptance | YES | NO | NO | NO |
| ProposalView | ProposalView | YES | NO | NO | NO |
| ContactRequest | ContactRequest | YES | YES | YES | YES |
| ProposalFollowUp | ProposalFollowUp | YES | YES | YES | YES |
| EvidenceSnapshot | EvidenceSnapshot | YES | YES | YES | YES |
| ProposalTemplate | ProposalTemplate | YES | YES | YES | YES |
| User | User | YES | YES | YES | YES |
| Invitation | Invitation | YES | YES | YES | YES |
| Playbook | Playbook | YES | YES | YES | YES |
| AuditSchedule | AuditSchedule | YES | YES | YES | YES |
| AuditTarget | AuditTarget | YES | YES | YES | YES |
| ProspectDiscoveryJob | ProspectDiscoveryJob | YES | YES | YES | YES |
| ProspectLead | ProspectLead | YES | YES | YES | YES |
| ProspectEnrichmentRun | ProspectEnrichmentRun | YES | YES | YES | YES |
| OutreachSendingDomain | OutreachSendingDomain | YES | YES | YES | YES |
| OutreachDomainDailyStat | OutreachDomainDailyStat | YES | YES | YES | YES |
| OutreachEmail | OutreachEmail | YES | YES | YES | YES |
| OutreachEmailEvent | OutreachEmailEvent | YES | YES | YES | YES |
| ApiKey | ApiKey | YES | YES | YES | YES |
| TenantBranding | TenantBranding | YES | YES | YES | YES |
| ProspectStateTransition | ProspectStateTransition | YES | YES | YES | YES |
| DeliveryTask | DeliveryTask | YES | YES | YES | YES |
| PipelineConfig | PipelineConfig | YES | YES | YES | YES |
| PipelineErrorLog | PipelineErrorLog | YES | YES | YES | YES |
| OutreachTemplatePerformance | OutreachTemplatePerformance | YES | NO | NO | NO |
| WinLossRecord | WinLossRecord | YES | YES | YES | YES |
| PreWarmingAction | PreWarmingAction | YES | YES | YES | YES |
| DetectedSignal | DetectedSignal | YES | YES | YES | YES |
| ChatConversation | ChatConversation | YES | YES | YES | YES |
| PartnerDeliveredLead | PartnerDeliveredLead | YES | NO | NO | NO |
| ProposalOutreach | ProposalOutreach | YES | YES | YES | YES |
| FollowUpEmailSend | FollowUpEmailSend | YES | YES | YES | YES |
| UsageRecord | UsageRecord | YES | YES | YES | YES |
| FailedWebhookEvent | failed_webhook_events | YES | NO | NO | NO |
| CartAbandonmentEvent | cart_abandonment_events | YES | NO | NO | NO |
| Subscription | subscriptions | YES | NO | NO | NO |
| Payment | payments | YES | NO | NO | NO |
| CheckoutAttempt | checkout_attempts | YES | NO | NO | NO |
| GeneratedArtifact | GeneratedArtifact | YES | NO | NO | NO |
| DeliveryBundle | DeliveryBundle | YES | NO | NO | NO |
| AdversarialQARun | AdversarialQARun | YES | NO | NO | NO |
| HallucinationLog | HallucinationLog | YES | NO | NO | NO |
| HumanReviewFlag | HumanReviewFlag | YES | NO | NO | NO |
| Project | Project | YES | YES | YES | YES |
| NPSSurvey | NPSSurvey | YES | YES | YES | YES |
| QATelemetry | QATelemetry | YES | YES | YES | YES |
| Metric | Metric | YES | NO | NO | NO |
| AuditTrailEvent | AuditTrailEvent | YES | NO | NO | NO |
| MonitoringConfig | MonitoringConfig | YES | NO | NO | NO |
| LocationGroup | LocationGroup | YES | NO | NO | NO |
| CircuitBreakerState | CircuitBreakerState | YES | NO | NO | NO |
| DeadLetterQueue | DeadLetterQueue | YES | NO | NO | NO |
| ClientDashboard | ClientDashboard | YES | NO | NO | NO |
| UpsellOpportunity | UpsellOpportunity | YES | NO | NO | NO |
| NotificationPreference | NotificationPreference | YES | NO | NO | NO |
| ScheduledAuditRun | ScheduledAuditRun | YES | NO | NO | NO |
| CompetitorSignal | CompetitorSignal | YES | NO | NO | NO |
| ReEngagementCampaign | ReEngagementCampaign | YES | NO | NO | NO |
| WinBackCampaign | WinBackCampaign | YES | NO | NO | NO |
| PromptVersion | PromptVersion | YES | NO | NO | NO |
| PromptPerformanceLog | PromptPerformanceLog | YES | NO | NO | NO |
| ABExperiment | ABExperiment | YES | NO | NO | NO |
| Prediction | Prediction | YES | NO | NO | NO |
| Scenario | Scenario | YES | NO | NO | NO |
Strengths:
- uses
current_setting('app.current_tenant_id', true)consistently - uses
ENABLE ROW LEVEL SECURITY - uses
FORCE ROW LEVEL SECURITY - creates
app_user - ships with a matching revert file
Weaknesses:
- only
34explicit policies in the file body - no
WITH CHECKclauses for writes - no explicit admin/system bypass policy
- incomplete model coverage
- some comments are stale (for example, comments imply some tables lack direct
tenantIdwhen schema now includes it)
Strengths:
- attempts to cover some newer models absent from the migration-style file
Weaknesses:
- no
FORCE ROW LEVEL SECURITY - no
WITH CHECKclauses - no role creation or privilege model
- targets Prisma model names instead of physical table names for some
@@mapped models - not paired with a tracked revert
The tracked file appears to target model names rather than physical table names for mapped models. Examples:
FailedWebhookEventinstead offailed_webhook_eventsCartAbandonmentEventinstead ofcart_abandonment_eventsSubscriptioninstead ofsubscriptionsPaymentinstead ofpayments
These policies either never applied successfully or target non-existent relations.
Nullable tables in the migration-style file use predicates such as:
"tenantId" IS NULL OR "tenantId" = current_setting('app.current_tenant_id', true)That allows system-wide rows to remain visible. This may be intentional, but it is a policy choice that needs to be explicit and reviewed per table.
prisma/migrations/**exists on disk but is ignored by gitgit log --all --oneline -- prisma/migrations/rls/returned no tracked historygit ls-files prisma/migrations/rls prisma/enable_rls.sql prisma/migrations/migration_lock.tomlreturned onlyprisma/enable_rls.sql
There is currently no tracked migration history proving the stronger migration-style RLS SQL was ever committed, reviewed, or deployed in a repeatable way.
SELECT schemaname, tablename, rowsecurity, forcerowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;These queries are the source of truth for whether RLS is actually enabled and forced in the live environment.
Current behavior in lib/prisma.ts:
- global Prisma
$extendsquery hook wraps$allModels / $allOperations - reads tenant context from AsyncLocalStorage via
getTenantIdFromStore() - if tenant context exists, opens an interactive transaction and executes:
SET LOCAL app.current_tenant_id = '<tenantId>'- then executes the Prisma model query
- if tenant context is absent, it runs the query unmodified
- the hook attempts to scope every Prisma model operation
- raw client-level SQL paths such as
$queryRaw,$executeRaw, and$executeRawUnsafeare outside this model-hook coverage and can bypass Prisma-layer protections - behavior under nested transactions is not proven from static inspection alone
SET LOCALonly lives for the current transaction; if the query does not actually run on the same transaction connection, RLS will not see the tenant setting
The repo assumes SET LOCAL remains viable with its pooled connection design. That is exactly the behavior that must be validated under local Postgres plus PgBouncer.
- docs/pgbouncer-setup.md documents:
- direct/admin URL on
5432 - pooled application URL on
6432 - PgBouncer in transaction mode
- direct/admin URL on
- docker-compose.pgbouncer.yml sets:
PGBOUNCER_POOL_MODE=transaction
prisma/schema.prisma currently uses:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}There is no active directUrl or DIRECT_URL in the Prisma datasource.
- the app is designed around a pooled application
DATABASE_URL - the repo explicitly models transaction-mode PgBouncer as the expected production architecture
- therefore the current RLS shim depends on
SET LOCALworking correctly in a transaction-pooled environment - this is a known footgun and needs live validation before any confidence is warranted
- lib/tenant/tests/isolation.test.ts uses a real
PrismaClient - lib/tenant/tests/isolation-stress.test.ts also uses a real
PrismaClient
They prove:
- local multi-tenant patterns can be tested against a real DB
- the codebase has at least some intentional tenant isolation coverage
They do not prove:
- that the live environment actually has RLS enabled
- that the live environment uses
FORCE ROW LEVEL SECURITY - that the live app connects as a restricted role rather than
postgres - that
SET LOCALbehaves correctly under the production pooled connection model
gcloud config get-value projectreturnedixcc-486621gcloud projects listsucceeded and showed visible projects including:ixcc-486621proposal-487522swinglabs-funddrape-proddealpilot-aeblazecrawl-prod
gcloud sql instances list --project=ixcc-486621succeeded- Secret Manager access for
DATABASE_URLandDIRECT_URLinixcc-486621succeeded
- Cloud Run service listing in
ixcc-486621is still denied:
ERROR: (gcloud.run.services.list) PERMISSION_DENIED:
Permission 'run.services.list' denied on resource 'namespaces/ixcc-486621/services'
- Cloud Run job listing in
ixcc-486621is still denied:
ERROR: (gcloud.run.jobs.list) PERMISSION_DENIED:
Permission 'run.jobs.list' denied on resource 'namespaces/ixcc-486621/jobs'
- Direct
describecalls for the likely ProposalOS services are also denied:proposal-engineproposal-engine-staging
So post-auth access improved, but not enough to inspect actual Cloud Run runtime env or Cloud SQL attachment metadata for ProposalOS in ixcc-486621.
Visible instances:
immigration-db—northamerica-northeast1—RUNNABLEimmigration-prod—northamerica-northeast1—RUNNABLEproposal-engine-db—us-central1—STOPPEDmisprice-db—us-central1—RUNNABLE
This confirms again that proposal-engine-db remains stopped and is not the active secret-backed target today.
Accessible secrets:
DATABASE_URLDIRECT_URL
Latest DATABASE_URL versions list:
- version
18— enabled —2026-02-14T21:20:45 - version
17— enabled —2026-02-07T19:31:05 - version
16— enabled —2026-02-07T19:23:16
Masked DATABASE_URL shape:
postgresql://postgres:REDACTED@/immigration_platform?host=/cloudsql/ixcc-486621:northamerica-northeast1:immigration-prod
Parsed fields:
- role:
postgres - database:
immigration_platform - Cloud SQL socket target:
ixcc-486621:northamerica-northeast1:immigration-prod
DIRECT_URL resolved to the same masked shape and the same parsed role/database/instance.
- The current
DATABASE_URLandDIRECT_URLsecrets inixcc-486621are not pointing atproposal-engine-db - They point to
immigration-prod/immigration_platform - The current connecting DB role encoded in both secrets is
postgres
This is a strong signal that the visible DB credentials in this project are for another live app in the shared project, not for the dormant proposal-engine-db.
Because Cloud Run access remains denied in ixcc-486621, this audit still cannot prove:
- whether a live ProposalOS Cloud Run service exists in that project,
- whether ProposalOS consumes
DATABASE_URL/DIRECT_URL, - whether any ProposalOS runtime is actually attached to
immigration-prod, - or whether a different secret or different project backs ProposalOS runtime traffic.
- **
prisma/migrations/**is gitignored.** This is a source-of-truth failure for schema and RLS history across machines and deploys. - Two divergent RLS files exist. The stronger one is untracked; the tracked one is weaker and partially wrong.
- The tracked file has
@@maptable-name bugs. Policies forSubscription,Payment,FailedWebhookEvent, andCartAbandonmentEventappear to target model names, not physical tables. - The tracked file lacks
FORCE ROW LEVEL SECURITY. If the app connects as the table owner, RLS may be bypassed silently. - The repo assumes Prisma + PgBouncer transaction mode +
SET LOCALis safe. That assumption is unproven and must be validated before rollout.
- Restore migration tracking in git before touching RLS behavior.
- Reconcile the two RLS files into one authoritative tracked artifact.
- Validate
SET LOCAL app.current_tenant_idunder:- direct local Postgres
- local PgBouncer transaction mode
- Confirm live deployment details after
gcloudauth refresh:- Cloud Run service name
- Cloud SQL attachment
- DB role used by the app
- Expand policy coverage from the current partial state to full tenant-bearing model coverage.
- After auth refresh, which Cloud Run service is the real ProposalOS production target?
- Does the live app connect as
postgres,app_user, or another role? - Was
prisma/migrations/**intentionally gitignored at some point, or is it pure drift? - Should nullable
tenantIdrows remain globally visible on all such tables, or only on a narrower allowlist?
R2— reconcile legacy RLS files into a standard Prisma migration directory- canonical merged artifact:
- legacy files removed:
prisma/enable_rls.sqlprisma/migrations/rls/enable_rls.sqlprisma/migrations/rls/revert_rls.sql
- migration tracking restored in git
- Cloud SQL inventory in
ixcc-486621is visible - Cloud Run runtime inspection in
ixcc-486621is still IAM-blocked DATABASE_URLandDIRECT_URLboth point to:- DB role:
postgres - DB name:
immigration_platform - Cloud SQL socket:
ixcc-486621:northamerica-northeast1:immigration-prod
- DB role:
proposal-engine-dbremains stopped and is not the current secret-backed target
vitest: full suite did not complete within the closure window; process was terminated after extended waittsc --noEmit: still failing with pre-existing type errors in test/eval code and config, including:tests/red-team/adversarialQA.eval.tsvitest.config.deep-localization.ts
pnpm build: completed successfully
- No application code changed in this phase
- Changes were limited to:
- docs
.gitignore- migration SQL files
tscremains red in the same broad Phase 3 /P1-01territory captured in PHASE-2-BASELINE.mdbuildremains greenvitestfull-suite comparison is inconclusive because the suite did not finish in-window
- RLS audit materialized to a tracked artifact
-
prisma/migrations/**restored to git - Divergent RLS files reconciled into one canonical Prisma migration directory
-
@@maptable-name bugs corrected for known mapped tables -
WITH CHECKclauses added to the merged migration - Post-auth Cloud SQL and secret verification completed
- Cloud Run runtime inspection still blocked by IAM in
ixcc-486621 - Live DB migration state still not confirmed via
pg_tables/pg_policies - Remaining RLS coverage gaps (
17tenant-bearing models) deferred to Phase 2.2+ - Tenant-derived models missing
tenantIddeferred to Phase 2.3 schema work