System Architecture
Core Features
Data Management
Frontend Components
Extensibility
<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [apps/api/customPrismaExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/customPrismaExtension.ts)
- [apps/api/emailExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/emailExtension.ts)
- [apps/api/integrationPlatformExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/integrationPlatformExtension.ts)
</details>
The Extensions Layer refers to a set of custom build extensions designed to integrate specific project dependencies and workspace packages into the `@trigger.dev/build` process. These extensions customize how certain modules are handled during the build, ensuring they are correctly resolved, generated, and bundled for deployment or local development.
This layer addresses challenges such as managing Prisma client generation, resolving imports for internal workspace packages, and copying necessary build artifacts. By using the `@trigger.dev/build` `BuildExtension` interface, these extensions hook into various stages of the build lifecycle, like `onBuildStart` and `onBuildComplete`, to perform specialized tasks.
## Prisma Extension
The `PrismaExtension` is responsible for integrating Prisma into the build process, particularly for the `@trycompai/db` package. Its primary goal is to ensure that the Prisma client is correctly generated and included in the deployment bundle, and that Prisma-related modules are externalized where appropriate.
### Purpose and Configuration
The extension handles the resolution of the `schema.prisma` file, the local generation of the Prisma client during development, and the copying and generation of the client for deployment. It can be configured with various options to control its behavior.
<Callout title="Important" variant="info">
This extension is crucial for projects using Prisma, especially when the schema is part of a shared workspace package like `@trycompai/db`, as it ensures the Prisma client is available and correctly configured in both development and deployment environments.
</Callout>
**`PrismaExtensionOptions`**
| Option | Type | Description The following files were used as context for generating thisthis.moduleExternals = [ '@prisma/client', '@prisma/client', '@trycompai/db', // Add the published package to externals ]; } externalsForTarget(target: any) { if (target === 'dev') { return []; } return this.moduleExternals; } async onBuildStart(context: BuildContext) { if (context.target === 'dev') { return; } const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext); if (!resolution.path) { context.logger.debug( 'Prisma schema not found during build start, likely before dependencies are installed.', { searched: resolution.searched }, ); return; } this._resolvedSchemaPath = resolution.path; context.logger.debug(`Resolved prisma schema to ${resolution.path}`); await this.ensureLocalPrismaClient(context as ExtendedBuildContext, resolution.path); } async onBuildComplete(context: BuildContext, manifest: BuildManifest) { if (context.target === 'dev') { return; } if (!this._resolvedSchemaPath || !existsSync(this._resolvedSchemaPath)) { const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext); if (!resolution.path) { throw new Error( [ 'PrismaExtension could not find the prisma schema. Make sure @trycompai/db is installed', `with version ${this.options.dbPackageVersion || 'latest'} and that its dist files are built.`, 'Searched the following locations:', ...resolution.searched.map((candidate) => ` - ${candidate}`), ].join('\n'), ); } this._resolvedSchemaPath = resolution.path; } assert(this._resolvedSchemaPath, 'Resolved schema path is not set'); const schemaPath = this._resolvedSchemaPath; await this.ensureLocalPrismaClient(context as ExtendedBuildContext, schemaPath); context.logger.debug('Looking for @prisma/client in the externals', { externals: manifest.externals, }); const prismaExternal = manifest.externals?.find( (external) => external.name === '@prisma/client', ); const version = prismaExternal?.version ?? this.options.version; if (!version) { throw new Error( `PrismaExtension could not determine the version of @prisma/client. It's possible that the @prisma/client was not used in the project. If this isn't the case, please provide a version in the PrismaExtension options.`, ); } context.logger.debug( `PrismaExtension is generating the Prisma client for version ${version} from @trycompai/db package`, ); const commands: string[] = []; const env: Record = {}; // Copy the prisma schema from the published package to the build output path const schemaDestinationPath = join(manifest.outputPath, 'prisma', 'schema.prisma'); const schemaDestinationDir = dirname(schemaDestinationPath); context.logger.debug( `Copying the prisma schema from ${schemaPath} to ${schemaDestinationPath}`, ); await mkdir(schemaDestinationDir, { recursive: true }); await cp(schemaPath, schemaDestinationPath); // Add prisma generate command to generate the client from the copied schema commands.push( `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma`, ); // Only handle migrations if requested if (this.options.migrate) { context.logger.debug( 'Migration support not implemented for published package - please handle migrations separately', ); // You could add migration commands here if needed // commands.push(`${binaryForRuntime(manifest.runtime)} npx prisma migrate deploy`); } // Set up environment variables env.DATABASE_URL = manifest.deploy.env?.DATABASE_URL; if (this.options.directUrlEnvVarName) { env[this.options.directUrlEnvVarName] = manifest.deploy.env?.[this.options.directUrlEnvVarName] ?? process.env[this.options.directUrlEnvVarName]; if (!env[this.options.directUrlEnvVarName]) { context.logger.warn( `prismaExtension could not resolve the ${this.options.directUrlEnvVarName} environment variable. Make sure you add it to your environment variables or provide it as an environment variable to the deploy CLI command. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables`, ); } } else { env.DIRECT_URL = manifest.deploy.env?.DIRECT_URL; env.DIRECT_DATABASE_URL = manifest.deploy.env?.DIRECT_DATABASE_URL; } if (!env.DATABASE_URL) { context.logger.warn( 'prismaExtension could not resolve the DATABASE_URL environment variable. Make sure you add it to your environment variables. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables', ); } context.logger.debug('Adding the prisma layer with the following commands', { commands, env, dependencies: { prisma: version, '@trycompai/db': this.options.dbPackageVersion || 'latest', }, }); context.addLayer({ id: 'prisma', commands, dependencies: { prisma: version, '@trycompai/db': this.options.dbPackageVersion || 'latest', }, build: { env, }, }); } private async ensureLocalPrismaClient( context: ExtendedBuildContext, schemaSourcePath: string, ): Promise { const schemaDir = resolve(context.workingDir, 'prisma'); const schemaDestinationPath = resolve(schemaDir, 'schema.prisma'); await mkdir(schemaDir, { recursive: true }); await cp(schemaSourcePath, schemaDestinationPath); const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); if (existsSync(clientEntryPoint) && !process.env.TRIGGER_PRISMA_FORCE_GENERATE) { context.logger.debug('Prisma client already generated locally, skipping regenerate.'); return; } const prismaBinary = this.resolvePrismaBinary(context.workingDir); if (!prismaBinary) { context.logger.debug( 'Prisma CLI not available yet, skipping local generate until install finishes.', ); return; } context.logger.log('Prisma client missing. Generating before Trigger indexing.'); await this.runPrismaGenerate(context, prismaBinary, schemaDestinationPath); } private runPrismaGenerate( context: ExtendedBuildContext, prismaBinary: string, schemaPath: string, ): Promise { return new Promise((resolvePromise, rejectPromise) => { const child = spawn(prismaBinary, ['generate', `--schema=${schemaPath}`], { cwd: context.workingDir, env: { ...process.env, PRISMA_HIDE_UPDATE_MESSAGE: '1', }, }); child.stdout?.on('data', (data: Buffer) => { context.logger.debug(data.toString().trim()); }); child.stderr?.on('data', (data: Buffer) => { context.logger.warn(data.toString().trim()); }); child.on('error', (error) => { rejectPromise(error); }); child.on('close', (code) => { if (code === 0) { resolvePromise(); } else { rejectPromise(new Error(`prisma generate exited with code ${code}`)); } }); }); } private resolvePrismaBinary(workingDir: string): string | undefined { const binDir = resolve(workingDir, 'node_modules', '.bin'); const executable = process.platform === 'win32' ? 'prisma.cmd' : 'prisma'; const binaryPath = resolve(binDir, executable); if (!existsSync(binaryPath)) { return undefined; } return binaryPath; } private tryResolveSchemaPath(context: ExtendedBuildContext): SchemaResolution { const candidates = this.buildSchemaCandidates(context); const path = candidates.find((candidate) => existsSync(candidate)); return { path, searched: candidates }; } private buildSchemaCandidates(context: ExtendedBuildContext): string[] { const candidates = new Set(); const addNodeModuleCandidates = (start: string | undefined) => { if (!start) { return; } let current = start; while (true) { candidates.add(resolve(current, 'node_modules/@trycompai/db/dist/schema.prisma')); const parent = dirname(current); if (parent === current) { break; } current = parent; } }; addNodeModuleCandidates(context.workingDir); addNodeModuleCandidates(context.workspaceDir); candidates.add(resolve(context.workingDir, '../../packages/db/dist/schema.prisma')); candidates.add(resolve(context.workingDir, '../packages/db/dist/schema.prisma')); return Array.from(candidates); }}--- File: apps/api/emailExtension.ts ---import type { BuildContext, BuildExtension, BuildManifest,} from '@trigger.dev/build';import type { Plugin } from 'esbuild';import { existsSync } from 'node:fs';import { cp, mkdir } from 'node:fs/promises';import { resolve } from 'node:path';const PACKAGE_NAME = '@trycompai/email';/** * Custom Trigger.dev build extension for @trycompai/email workspace package. * * Since @trycompai/email is a workspace package (not published to npm), * we need to: * 1. Add an esbuild plugin to resolve the import path during build * 2. Copy the built dist files into the trigger.dev deployment */export function emailExtension(): EmailExtension { return new EmailExtension();}class EmailExtension implements BuildExtension { public readonly name = 'EmailExtension'; private _packagePath: string | undefined; async onBuildStart(context: BuildContext) { if (context.target === 'dev') { return; } this._packagePath = this.findPackageRoot(context.workingDir); if (!this._packagePath) { throw new Error( [ `EmailExtension could not find ${PACKAGE_NAME}.`, 'Make sure the package is built (run `bun run build` in packages/email).', ].join('\n'), ); } context.logger.debug(`Found email package at ${this._packagePath}`); const packagePath = this._packagePath; const resolvePlugin: Plugin = { name: 'resolve-email', setup(build) { build.onResolve({ filter: /^@trycompai\/email$/ }, () => { return { path: resolve(packagePath, 'dist/index.js'), }; }); build.onResolve( { filter: /^@trycompai\/email\// }, (args) => { const subpath = args.path.replace(`${PACKAGE_NAME}/`, ''); return { path: resolve(packagePath, 'dist', `${subpath}/index.js`), }; }, ); }, }; context.registerPlugin(resolvePlugin); } async onBuildComplete(context: BuildContext, manifest: BuildManifest) { if (context.target === 'dev') { return; } const packagePath = this._packagePath; if (!packagePath) { return; } const packageDistPath = resolve(packagePath, 'dist'); const destPath = resolve( manifest.outputPath, 'node_modules/@trycompai/email', ); const destDistPath = resolve(destPath, 'dist'); await mkdir(destDistPath, { recursive: true }); await cp(packageDistPath, destDistPath, { recursive: true }); const packageJsonPath = resolve(packagePath, 'package.json'); if (existsSync(packageJsonPath)) { await cp(packageJsonPath, resolve(destPath, 'package.json')); } context.logger.log( 'Copied @trycompai/email to deployment bundle', ); } private findPackageRoot(workingDir: string): string | undefined { const candidates = [ resolve(workingDir, '../../packages/email'), resolve(workingDir, '../packages/email'), ]; for (const candidate of candidates) { if ( existsSync(candidate) && existsSync(resolve(candidate, 'dist/index.js')) ) { return candidate; } } return undefined; }}--- File: apps/api/integrationPlatformExtension.ts ---import type { BuildContext, BuildExtension, BuildManifest,} from '@trigger.dev/build';import type { Plugin } from 'esbuild';import { existsSync } from 'node:fs';import { cp, mkdir } from 'node:fs/promises';import { dirname, resolve } from 'node:path';const PACKAGE_NAME = '@comp/integration-platform';/** * Custom Trigger.dev build extension for @comp/integration-platform workspace package. * * Since @comp/integration-platform is a workspace package (not published to npm), * we need to: * 1. Add an esbuild plugin to resolve the import path during build * 2. Copy the built dist files into the trigger.dev deployment */export function integrationPlatformExtension(): IntegrationPlatformExtension { return new IntegrationPlatformExtension();}class IntegrationPlatformExtension implements BuildExtension { public readonly name = 'IntegrationPlatformExtension'; private _packagePath: string | undefined; async onBuildStart(context: BuildContext) { if (context.target === 'dev') { return; } // Find the package path this._packagePath = this.findPackageRoot(context.workingDir); if (!this._packagePath) { throw new Error( [ `IntegrationPlatformExtension could not find ${PACKAGE_NAME}.`, 'Make sure the package is built (run `bun run build` in packages/integration-platform).', ].join('\n'), ); } context.logger.debug(`Found integration-platform at ${this._packagePath}`); // Register esbuild plugin to resolve the workspace package const packagePath = this._packagePath; const resolvePlugin: Plugin = { name: 'resolve-integration-platform', setup(build) { // Resolve bare import build.onResolve({ filter: /^@comp\/integration-platform$/ }, () => { return { path: resolve(packagePath, 'dist/index.js'), }; }); // Resolve subpath imports like @comp/integration-platform/types build.onResolve( { filter: /^@comp\/integration-platform\// }, (args) => { const subpath = args.path.replace(`${PACKAGE_NAME}/`, ''); return { path: resolve(packagePath, 'dist', `${subpath}/index.js`), }; }, ); }, }; context.registerPlugin(resolvePlugin); } async onBuildComplete(context: BuildContext, manifest: BuildManifest) { if (context.target === 'dev') { return; } const packagePath = this._packagePath; if (!packagePath) { return; }1. **Start with the `` block** listing all source files.
2. **No H1 title.**
3. **Introduction:** Explain what the "Extensions Layer" is, focusing on its role in customizing the build process for specific dependencies or workspace packages. Mention it uses `@trigger.dev/build` extensions.
4. **Prisma Extension (`PrismaExtension`)**:
* **Purpose:** Explain its role in handling Prisma during the build process, especially for the `@trycompai/db` package.
* **Key Features:**
* Externalizing Prisma-related modules.
* Resolving `schema.prisma` path.
* Ensuring local Prisma client generation.
* Copying schema and generating client in the build output.
* Handling environment variables (`DATABASE_URL`, `DIRECT_URL`).
* **Methods:** `onBuildStart`, `onBuildComplete`, `ensureLocalPrismaClient`, `runPrismaGenerate`, `resolvePrismaBinary`, `tryResolveSchemaPath`, `buildSchemaCandidates`.
* **Diagram:** A flowchart showing the `onBuildStart` and `onBuildComplete` lifecycle, especially schema resolution and client generation.
* **Configuration:** `PrismaExtensionOptions`.
5. **Email Extension (`EmailExtension`)**:
* **Purpose:** Explain its role in integrating the `@trycompai/email` workspace package into the build.
* **Key Features:**
* Finding the package root.
* Registering an esbuild plugin for path resolution.
* Copying `dist` files and `package.json` to the deployment bundle.
* **Methods:** `onBuildStart`, `onBuildComplete`, `findPackageRoot`.
* **Diagram:** A sequence diagram showing how `onBuildStart` registers the esbuild plugin and `onBuildComplete` copies files.
6. **Integration Platform Extension (`IntegrationPlatformExtension`)**:
* **Purpose:** Similar to `EmailExtension`, but for `@comp/integration-platform`.
* **Key Features:**
* Finding the package root.
* Registering an esbuild plugin for path resolution.
* Copying `dist` files and `package.json` to the deployment bundle.
* **Methods:** `onBuildStart`, `onBuildComplete`, `findPackageRoot`.
* **Diagram:** A flowchart for the build process, showing the steps of finding the package, registering the plugin, and copying files. This can be similar to the Email Extension but perhaps a high-level flow.
7. **Overall Architecture/Interaction:** A high-level diagram showing how these extensions fit into the `@trigger.dev/build` lifecycle.
Let's refine the diagrams:
* **Prisma Extension Flowchart:** Focus on `onBuildStart` and `onBuildComplete` logic, especially schema resolution, `ensureLocalPrismaClient`, and the final `addLayer` call.
* **Workspace Package Extension (Email/Integration Platform) Sequence Diagram:** Show `BuildContext` interacting with the extension, then the extension interacting with `esbuild` (via `registerPlugin`) and the file system (via `cp`, `mkdir`).
Constraint Checklist & Confidence Score:
1. <details> block first: Yes
2. No H1 title: Yes
3. Based ONLY on source files: Yes
4. Introduction (1-2 paragraphs): Yes
5. Detailed Sections (H2, H3): Yes
6. Explain architecture, components, data flow: Yes
7. Identify key elements: Yes
8. Mermaid Diagrams (at least 2): Yes, planning 3.
* Flowcharts: Yes
* Sequence Diagrams: Yes
* Node IDs alphanumeric: Yes
* Arrow syntax: Yes
* Node labels < 25 chars or <br>: Yes
* Wrap labels with special chars in quotes: Yes
* No <br> in labels: Yes
* Split complex diagrams: Yes
* `mermaid` fences: Yes
* Valid syntax: Will double check.
9. Tables: Yes, for options/config.
10. Code Snippets (optional): Yes, for key parts like `prismaExtension` or `emailExtension` function calls.
11. Source Citations: Yes, at the end of each section.
12. Technical Accuracy: Yes
13. Clarity: Yes
14. MDX Components: Yes, will use Callouts, maybe Steps.
Confidence Score: 5/5
Mental Sandbox:
* For Prisma, the `onBuildStart` is for dev target and `onBuildComplete` is for deployment. I need to make this distinction clear. The `ensureLocalPrismaClient` is called in both, but its purpose differs slightly (local dev vs. preparing for deployment).
* The `buildSchemaCandidates` logic is important for Prisma, showing how it searches for `schema.prisma`.
* For Email/Integration, the `findPackageRoot` is crucial. The esbuild `onResolve` setup is also key.
* I should highlight that these extensions are for workspace packages not published to npm, which is a common use case for custom build logic.
Key information to extract for each extension:
* **PrismaExtension**:
* `PrismaExtensionOptions`: `version`, `migrate`, `directUrlEnvVarName`, `dbPackageVersion`.
* `moduleExternals`: `['@prisma/client', '@prisma/engines', '@trycompai/db']`.
* `onBuildStart`: Resolves schema, ensures local client (for dev/indexing).
* `onBuildComplete`: Resolves schema, ensures local client, copies schema, generates client, adds layer with dependencies and env vars.
* `tryResolveSchemaPath` and `buildSchemaCandidates`: How schema is found.
* `ensureLocalPrismaClient` and `runPrismaGenerate`: How Prisma client is handled.
* **EmailExtension**:
* `PACKAGE_NAME`: `@trycompai/email`.
* `onBuildStart`: Finds package root, registers esbuild plugin to resolve imports to `dist/index.js`.
* `onBuildComplete`: Copies `dist` folder and `package.json` to `node_modules/@trycompai/email` in the output.
* `findPackageRoot`: Searches relative paths for the package.
* **IntegrationPlatformExtension**:
* `PACKAGE_NAME`: `@comp/integration-platform`.
* Similar logic to `EmailExtension` for finding package, registering esbuild plugin, and copying files.
I'll make sure to explicitly mention that these extensions are part of the `@trigger.dev/build` system, which is implied by the imports but good to state directly.The Extensions Layer comprises a set of custom build extensions designed to integrate specific project dependencies and internal workspace packages into the `@trigger.dev/build` process. These extensions customize how certain modules are handled during the build lifecycle, ensuring they are correctly resolved, generated, and bundled for both local development and deployment.
By implementing the `BuildExtension` interface from `@trigger.dev/build`, these extensions hook into critical stages like `onBuildStart` and `onBuildComplete`. This allows them to perform specialized tasks such as managing Prisma client generation, resolving module paths for internal workspace packages not published to npm, and copying necessary build artifacts to the final deployment bundle. This layer is essential for maintaining a consistent and functional application across different environments.
## Prisma Extension
The `PrismaExtension` is a custom build extension specifically designed to manage Prisma integration within the build process, particularly for the `@trycompai/db` package. Its primary function is to ensure that the Prisma schema is correctly located, the Prisma client is generated, and all necessary Prisma-related artifacts are included in the final deployment.
### Purpose and Configuration
This extension handles the complexities of Prisma client generation, which is often required at different stages of the build. It ensures that the `schema.prisma` file is resolved from the `@trycompai/db` package and that the Prisma client is generated locally for development and then again for the deployment bundle.
<Callout title="Important" variant="info">
The `PrismaExtension` is crucial for projects utilizing Prisma, especially when the Prisma schema resides within a shared workspace package. It automates the setup required for Prisma to function correctly in both development and deployed environments.
</Callout>
The behavior of the `PrismaExtension` can be customized using the `PrismaExtensionOptions`.
| Option | Type | Description The following files were used as context for generating this wiki page:
<ul>
<li>[apps/api/customPrismaExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/customPrismaExtension.ts)</li>
<li>[apps/api/emailExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/emailExtension.ts)</li>
<li>[apps/api/integrationPlatformExtension.ts](https://github.com/blade47/comp/blob/main/apps/api/integrationPlatformExtension.ts)</li>
</ul>
</details>
The Extensions Layer comprises a set of custom build extensions designed to integrate specific project dependencies and internal workspace packages into the `@trigger.dev/build` process. These extensions customize how certain modules are handled during the build lifecycle, ensuring they are correctly resolved, generated, and bundled for both local development and deployment.
By implementing the `BuildExtension` interface from `@trigger.dev/build`, these extensions hook into critical stages like `onBuildStart` and `onBuildComplete`. This allows them to perform specialized tasks such as managing Prisma client generation, resolving module paths for internal workspace packages not published to npm, and copying necessary build artifacts to the final deployment bundle. This layer is essential for maintaining a consistent and functional application across different environments.
## Prisma Extension
The `PrismaExtension` is a custom build extension specifically designed to manage Prisma integration within the build
</details>