{"record":{"id":"6512cab43a38cb6d","repo":"JuliusBrussee/caveman","slug":"postgres-organization-scope-is-required","errorCode":null,"errorMessage":"postgres: organization scope is required","messagePattern":"postgres: organization scope is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/postgresconfig/postgresconfig.go","lineNumber":767,"sourceCode":"\t\t)\n\t\tSELECT coalesce(string_agg(violation, '; ' ORDER BY violation), '') FROM violations\n\t`).Scan(&violations)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"postgres: inspect resolver schema: %w\", err)\n\t}\n\tif violations != \"\" {\n\t\treturn fmt.Errorf(\"postgres: resolver isolation incomplete: %s\", violations)\n\t}\n\treturn nil\n}\n\n// WithOrg runs fn inside a transaction whose tenant GUC is transaction-local.\n// Empty scopes are rejected: tenant work must never degrade to an unscoped\n// query when RLS is the hard boundary.\nfunc WithOrg(ctx context.Context, pool *pgxpool.Pool, orgID string, fn func(pgx.Tx) error) error {\n\torgID = strings.TrimSpace(orgID)\n\tif orgID == \"\" {\n\t\treturn errors.New(\"postgres: organization scope is required\")\n\t}\n\ttx, err := pool.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = tx.Rollback(ctx) }()\n\tif _, err := tx.Exec(ctx, `SELECT set_config('app.current_organization_id', $1, true)`, orgID); err != nil {\n\t\treturn fmt.Errorf(\"postgres: set organization scope: %w\", err)\n\t}\n\tif err := fn(tx); err != nil {\n\t\treturn err\n\t}\n\treturn tx.Commit(ctx)\n}\n","sourceCodeStart":749,"sourceCodeEnd":782,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/shared/platform/postgresconfig/postgresconfig.go#L749-L782","documentation":"postgresconfig.WithOrg wraps tenant work in a transaction and sets app.current_organization_id (transaction-local) so row-level security has a scope. It rejects an empty or whitespace orgID up front because an unscoped transaction would let a query degrade to unfiltered access — the opposite of what the RLS boundary exists for. The error is thrown before pool.Begin, so no database resources are touched.","triggerScenarios":"Calling WithOrg(ctx, pool, \"\") or WithOrg(ctx, pool, \" \") — typically the org ID was extracted from a request context, JWT claim, or URL path that was absent (middleware did not set it, anonymous route, or upstream parsing produced an empty string).","commonSituations":"An endpoint registered before the org-extraction middleware; a background job invoked without the per-tenant parameter; a refactor switching from org slug to UUID leaving old callers passing \"\"; tests exercising the handler directly without seeding the org context value.","solutions":["Resolve the organization ID before the call and fail the request when it is missing (401/400) instead of attempting tenant work","Fix the middleware order so the org context value is populated for every route that reaches WithOrg","For non-tenant (platform-level) work, use an explicit non-WithOrg code path rather than passing an empty scope"],"exampleFix":"// before\norgID, _ := ctx.Value(orgKey).(string)\nerr := postgresconfig.WithOrg(ctx, pool, orgID, fn)\n\n// after\norgID, ok := ctx.Value(orgKey).(string)\nif !ok || strings.TrimSpace(orgID) == \"\" {\n\treturn errors.New(\"organization context missing\")\n}\nerr := postgresconfig.WithOrg(ctx, pool, orgID, fn)","handlingStrategy":"validation","validationCode":"orgID, ok := ctx.Value(orgContextKey).(string)\nif !ok || strings.TrimSpace(orgID) == \"\" {\n\treturn errors.New(\"organization context required\")\n}","typeGuard":"func hasOrgScope(ctx context.Context) bool {\n\torgID, ok := ctx.Value(orgContextKey).(string)\n\treturn ok && strings.TrimSpace(orgID) != \"\"\n}","tryCatchPattern":"if err := postgresconfig.WithOrg(ctx, pool, orgID, fn); err != nil {\n\tif strings.Contains(err.Error(), \"organization scope is required\") {\n\t\t// request reached tenant code without an org ID; check middleware order\n\t}\n\treturn err\n}","preventionTips":["Register org-extraction middleware before any route that performs tenant work","Reject org-less requests at the handler boundary with 400/401 so this guard never fires in production"],"tags":["postgres","multi-tenancy","rls","validation","api-misuse","go"],"backgroundTag":"missing-tenant-context","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}