NestJS Backend
Wire @stndrds/adapter-nestjs SchemaModule with all five adapters used by standards-platform.
This guide walks through wiring @stndrds/adapter-nestjs's SchemaModule with the five adapters used by standards-platform: Supabase, Meilisearch, Redis, BullMQ, and E2B.
Breaking in vNEXT: Actor is now the single canonical identity. userId is gone from TenantContext, services, repositories, and PolicyContext — use actorId. withTenantContext(tenantId, fn, actorId?) lost its userId argument. The TenantContextInterceptor auto-provisions an actor for authenticated users, so resolveUserId in the snippet below resolves an actor id (the parameter name is unchanged for compatibility, but downstream code reads it from actorId).
Full module setup
The snippet below mirrors standards-platform/apps/api/src/modules/shared/schema/schema.module.ts. Trim sections you don't need yet — start with Supabase only, then add the rest.
// @noverify
import {
BullMQOutboxSignal,
BullMQQueueAdapter,
BullMQScheduleAdapter,
BullMQWorkerAdapter,
} from "@stndrds/adapter-bullmq";
import { E2BSandboxProvider } from "@stndrds/adapter-e2b";
import { MeilisearchSearchAdapter } from "@stndrds/adapter-meilisearch";
import {
API_KEY_AUTH_CONTEXT,
SchemaModule as StandardSchemaModule,
} from "@stndrds/adapter-nestjs";
import { RedisCacheAdapter, RedisRealtimeTransport } from "@stndrds/adapter-redis";
import { SupabaseDatabaseAdapter } from "@stndrds/adapter-supabase";
import { formRegistry, registry, viewRegistry } from "@stndrds/schema";
import type { ApiKeyAuthContext } from "@stndrds/schema";
import type { User } from "@supabase/supabase-js";
import { SupabaseClient } from "@supabase/supabase-js";
import { Queue } from "bullmq";
import type { Database } from "@my-app/supabase"; // generated types
const redisUrl = process.env.REDIS_URL ?? "redis://localhost:6379";
const parsedRedis = new URL(redisUrl);
const redisConnection = {
host: parsedRedis.hostname,
port: Number(parsedRedis.port) || 6379,
...(parsedRedis.password ? { password: parsedRedis.password } : {}),
};
const agentQueue = new Queue("agent-runs", { connection: redisConnection });
const eventQueue = new Queue("event-processing", { connection: redisConnection });
const supabase = new SupabaseClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const searchAdapter = new MeilisearchSearchAdapter({
host: process.env.MEILISEARCH_HOST ?? "",
apiKey: process.env.MEILISEARCH_API_KEY ?? "",
indexPrefix: "myapp",
});
const DEFAULT_TENANT_ID = "00000000-0000-0000-0000-000000000000";
export const SchemaModule = StandardSchemaModule.forRoot({
// Database adapter — search is required when Meilisearch is enabled
adapter: new SupabaseDatabaseAdapter(supabase, {
search: searchAdapter,
}),
search: {
reconcileOnStartup: true,
reconcileIntervalMs: 2 * 60 * 60 * 1000,
fullReindexThreshold: 0.2,
},
events: {
enabled: true,
outboxSignal: new BullMQOutboxSignal(eventQueue),
},
cache: {
adapter: new RedisCacheAdapter({
url: redisUrl,
keyPrefix: "myapp:",
}),
},
registry,
viewRegistry,
formRegistry,
autoSync: true,
auth: {
requireUser: false,
resolveUserId(request: Request & { user?: User }) {
if (request.user?.id !== undefined) return request.user.id;
const apiKeyCtx = (request as unknown as Record<string | symbol, unknown>)[
API_KEY_AUTH_CONTEXT
] as ApiKeyAuthContext | undefined;
return apiKeyCtx?.userId ?? apiKeyCtx?.createdBy ?? null;
},
},
tenant: {
mode: "multi",
headerName: "X-Tenant-ID",
defaultTenantId: DEFAULT_TENANT_ID,
masterTenantId: DEFAULT_TENANT_ID,
},
objects: true,
queue: new BullMQQueueAdapter(agentQueue),
schedule: new BullMQScheduleAdapter(agentQueue),
agents: {
enabled: true,
agentWorker: new BullMQWorkerAdapter({
queueName: "agent-runs",
connection: redisConnection,
concurrency: 5,
}),
eventWorker: new BullMQWorkerAdapter({
queueName: "event-processing",
connection: redisConnection,
concurrency: 10,
}),
streamTransport: new RedisRealtimeTransport(redisUrl),
sandbox: {
provider: new E2BSandboxProvider(),
template: "stndrds-sandbox",
maxConcurrent: 10,
},
},
global: true,
});Module imports
Add SchemaModule alongside these standard modules in your AppModule:
ConfigModule.forRoot— loads.env/.env.localand makesConfigServiceavailable globally. Passvalidateto fail-fast on missing vars.SupabaseModule.forRootAsync(fromnestjs-supabase-js) — injects a typedSupabaseClientinto the DI container, driven byConfigService.ThrottlerModule.forRoot— rate-limits all routes. Pair withThrottlerGuardinAPP_GUARD.SchemaModule(your export from above) — global; provides all standards services, object providers, and auto-generated CRUD routes.TenantModule— resolvesX-Tenant-IDheader into the AsyncLocalStorage tenant context viaTenantResolverMiddleware.
Auth guard
standards exposes ApiKeyAuthGuard from @stndrds/adapter-nestjs. Combine it with a Supabase JWT guard that extends BaseSupabaseAuthGuard (from nestjs-supabase-js):
// @noverify
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "@stndrds/adapter-nestjs";
import { BaseSupabaseAuthGuard } from "nestjs-supabase-js";
import { SupabaseClient } from "@supabase/supabase-js";
@Injectable()
export class SupabaseGuard extends BaseSupabaseAuthGuard implements CanActivate {
constructor(
supabaseClient: SupabaseClient,
private readonly reflector: Reflector,
) {
super(supabaseClient);
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
}Register both guards as APP_GUARD providers in AppModule. ApiKeyAuthGuard resolves last and populates API_KEY_AUTH_CONTEXT on the request; SupabaseGuard runs first and attaches request.user. The resolveUserId callback in SchemaModule.forRoot reads from whichever is present.
The standards-platform implementation lives in apps/api/src/modules/auth/supabase.guard.ts.
Env vars
| Var | Required | Description |
|---|---|---|
SUPABASE_URL | yes | Your Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY | yes | Service role key — never expose to client |
MEILISEARCH_HOST | yes (for search) | URL of your Meili instance |
MEILISEARCH_API_KEY | yes (for search) | Admin key |
REDIS_URL | yes (for cache/queue) | Redis connection string |
E2B_API_KEY | yes (for agents) | E2B sandbox API key |
For a minimal setup using only Supabase, follow the tutorial chapter 2. You can omit the BullMQ, Redis, Meilisearch, and E2B sections until you need those capabilities.