Troubleshooting
Step-by-step debugging guides for common issues. Each section starts with the symptom you observe, explains the root cause, and walks through the fix.
Tenant Isolation
Queries return all rows (RLS not filtering)
Symptom: findMany() returns data from all tenants instead of the current one.
Diagnosis:
- Check if RLS is enabled and forced:
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname = 'your_table_name';Both relrowsecurity and relforcerowsecurity must be true. If relforcerowsecurity is false, the table owner bypasses RLS:
ALTER TABLE your_table_name FORCE ROW LEVEL SECURITY;- Check your connection role:
SELECT current_user, current_setting('is_superuser');Superusers bypass RLS entirely. Create a dedicated application role:
CREATE ROLE app_user LOGIN PASSWORD 'secret';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;Update your DATABASE_URL to use this role.
- Check if the policy exists:
SELECT * FROM pg_policies WHERE tablename = 'your_table_name';If empty, create the policy:
CREATE POLICY tenant_isolation ON your_table_name
USING (tenant_id = current_setting('app.current_tenant', true)::text);- Run the CLI check:
npx @nestarc/tenancy checkThis detects drift between your Prisma schema and SQL policies.
Queries return zero rows (but data exists)
Symptom: findMany() returns [] even though the table has data.
Root cause: Tenant context is not being set, so current_setting('app.current_tenant') returns NULL, which matches no rows.
Diagnosis:
- Verify the tenant header is being sent:
curl -v http://localhost:3000/users -H "X-Tenant-Id: your-tenant-id"Check that the X-Tenant-Id header appears in the request.
- Verify the extractor is configured correctly:
TenancyModule.forRoot({
tenantExtractor: 'X-Tenant-Id', // must match the header name exactly
})- Check that
set_configis running inside the transaction:
Add temporary logging to your Prisma extension to confirm the tenant ID is being set:
SELECT current_setting('app.current_tenant', true);If this returns NULL or empty string, the set_config call isn't reaching the database.
- Check the
tenant_idvalues in your data:
SELECT DISTINCT tenant_id FROM your_table_name;Ensure the value you're sending in X-Tenant-Id matches exactly (case-sensitive).
Audit Logging
Audit records are not being created
Symptom: CUD operations succeed, but no rows appear in the audit_logs table.
Diagnosis:
- Check
trackedModelsconfiguration:
AuditLogModule.forRoot({
trackedModels: ['User', 'Task'], // model names must match Prisma schema exactly
})Model names are case-sensitive. user does not match User.
- Check that the extended Prisma client is being used:
The audit extension only works when queries go through the extended client. If you're using a raw PrismaClient instance (without $extends), writes are not tracked.
- Check for
@NoAudit()decorator:
If the route or controller has @NoAudit(), audit tracking is skipped for that handler.
- Check the database for errors:
Audit inserts run best-effort — they don't fail the business operation. Check your application logs for warnings like:
[AuditLog] Warning: Failed to insert audit record: ...- Verify the
audit_logstable exists:
SELECT * FROM information_schema.tables WHERE table_name = 'audit_logs';If it doesn't exist, run the migration:
npx prisma migrate devAudit records have null tenant_id
Symptom: Audit records are created but tenant_id is always null.
Root cause: @nestarc/tenancy is either not installed or the tenant context is not available when the audit extension runs.
Fix: Ensure TenancyModule is imported before AuditLogModule in your AppModule:
@Module({
imports: [
TenancyModule.forRoot({ ... }), // first
AuditLogModule.forRoot({ ... }), // second
],
})
export class AppModule {}And ensure the Prisma extension chain has tenancy first:
const prisma = new PrismaClient()
.$extends(createPrismaTenancyExtension(tenancyService)) // first
.$extends(createAuditExtension(auditOpts)); // secondPrisma Extensions
"Cannot read properties of undefined" in extension chain
Symptom: Runtime error when chaining multiple $extends calls.
Root cause: Extensions must be chained sequentially, not applied to the same base client:
// Wrong — both extensions receive the un-extended base client
const ext1 = prisma.$extends(tenancyExtension);
const ext2 = prisma.$extends(auditExtension); // does NOT include tenancy
// Correct — each extension wraps the previous result
const extended = prisma
.$extends(tenancyExtension)
.$extends(auditExtension);See the Prisma Extension Chaining guide for the full pattern.
Soft-deleted records still appearing in queries
Symptom: Records with a deletedAt timestamp still show up in findMany() results.
Diagnosis:
- Verify the model is in
softDeleteModels:
SoftDeleteModule.forRoot({
softDeleteModels: ['User', 'Post'], // check your model is listed
})- Check the
deletedAtcolumn name:
By default, the extension looks for deletedAt. If your column has a different name (e.g., deleted_at), configure it:
SoftDeleteModule.forRoot({
softDeleteModels: ['User'],
deletedAtField: 'deleted_at',
})- Check for
@WithDeleted()decorator:
If the route has @WithDeleted(), deleted records are intentionally included.
Still Stuck?
- Check the FAQ for quick answers to common questions
- Search GitHub Discussions for similar issues
- Open a GitHub Issue with reproduction steps