Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/config/extensions/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Trigger.dev provides a set of built-in extensions that you can use to customize
| [additionalPackages](/config/extensions/additionalPackages) | Install additional npm packages in your build image |
| [syncEnvVars](/config/extensions/syncEnvVars) | Automatically sync environment variables from external services to Trigger.dev |
| [syncVercelEnvVars](/config/extensions/syncEnvVars#syncVercelEnvVars) | Automatically sync environment variables from Vercel to Trigger.dev |
| [syncSupabaseEnvVars](/config/extensions/syncEnvVars#syncSupabaseEnvVars) | Automatically sync environment variables from Supabase to Trigger.dev |
| [esbuildPlugin](/config/extensions/esbuildPlugin) | Add existing or custom esbuild extensions to customize your build process |
| [emitDecoratorMetadata](/config/extensions/emitDecoratorMetadata) | Enable `emitDecoratorMetadata` in your TypeScript build |
| [audioWaveform](/config/extensions/audioWaveform) | Add Audio Waveform to your build image |
Expand Down
69 changes: 69 additions & 0 deletions docs/config/extensions/syncEnvVars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,72 @@ The extension syncs the following environment variables (with optional prefix):
- `POSTGRES_PRISMA_URL` - Connection string optimized for Prisma
- `POSTGRES_HOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`
- `PGHOST`, `PGHOST_UNPOOLED`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`

### syncSupabaseEnvVars

The `syncSupabaseEnvVars` build extension syncs environment variables from your Supabase project to Trigger.dev. It uses [Supabase Branching](https://supabase.com/docs/guides/deployment/branching) to automatically detect branches and build the appropriate database connection strings and API keys for your environment.

<AccordionGroup>
<Accordion title="Setting up authentication">
You need to set the `SUPABASE_ACCESS_TOKEN` and `SUPABASE_PROJECT_ID` environment variables, or pass them
as arguments to the `syncSupabaseEnvVars` build extension.

You can generate a `SUPABASE_ACCESS_TOKEN` in your Supabase [dashboard](https://supabase.com/dashboard/account/tokens).
</Accordion>

<Accordion title="Running in Vercel environment">
When running the build from a Vercel environment (determined by checking if the `VERCEL`
environment variable is present), this extension is skipped entirely.
</Accordion>
</AccordionGroup>

<Note>
This extension is skipped for `prod` and `dev` environments. It is designed to sync
branch-specific database connections and API keys for preview/staging environments using Supabase
Branching.
</Note>

```ts
import { defineConfig } from "@trigger.dev/sdk";
import { syncSupabaseEnvVars } from "@trigger.dev/build/extensions/core";

export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// This will automatically use the SUPABASE_ACCESS_TOKEN and SUPABASE_PROJECT_ID environment variables
extensions: [syncSupabaseEnvVars()],
},
});
```

Or you can pass in the token, project ID, and other options as arguments:

```ts
import { defineConfig } from "@trigger.dev/sdk";
import { syncSupabaseEnvVars } from "@trigger.dev/build/extensions/core";

export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
syncSupabaseEnvVars({
projectId: "your-supabase-project-id",
supabaseAccessToken: "your-supabase-access-token", // optional, we recommend to keep it as env variable
branch: "your-branch-name", // optional, defaults to ctx.branch
envVarPrefix: "MY_PREFIX_", // optional, prefix for all synced env vars
}),
],
},
});
```

The extension syncs the following environment variables (with optional prefix):

- `DATABASE_URL`, `POSTGRES_URL`, `SUPABASE_DB_URL` — PostgreSQL connection strings
- `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` — Individual connection parameters
- `SUPABASE_URL` — Supabase API URL
- `SUPABASE_ANON_KEY` — Anonymous API key
- `SUPABASE_SERVICE_ROLE_KEY` — Service role API key
- `SUPABASE_JWT_SECRET` — JWT secret
1 change: 1 addition & 0 deletions packages/build/src/extensions/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./core/aptGet.js";
export * from "./core/ffmpeg.js";
export * from "./core/neonSyncEnvVars.js";
export * from "./core/vercelSyncEnvVars.js";
export * from "./core/syncSupabaseEnvVars.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# List changeset files to see if one was added for this PR
ls -la .changeset/ 2>/dev/null | grep -v "^\." | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 526


🏁 Script executed:

# Check the content of any changeset files that might be new (excluding config)
find .changeset -type f -name "*.md" | head -20 | xargs -I {} sh -c 'echo "=== {} ===" && head -10 {}'

Repository: triggerdotdev/trigger.dev

Length of output: 1276


🏁 Script executed:

# Verify the file in question exists and shows the correct line
cat -n packages/build/src/extensions/core.ts | head -15

Repository: triggerdotdev/trigger.dev

Length of output: 455


LGTM on the re-export pattern. A changeset for @trigger.dev/build is required for this public package modification—add one using pnpm run changeset:add.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/build/src/extensions/core.ts` at line 8, Add a changeset for the
public package `@trigger.dev/build` to record the re-export change; run the
command `pnpm run changeset:add` and create a changeset entry describing the
export of "./core/syncSupabaseEnvVars.js" from
packages/build/src/extensions/core.ts so the package version and changelog are
updated accordingly.

232 changes: 232 additions & 0 deletions packages/build/src/extensions/core/syncSupabaseEnvVars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { BuildExtension } from "@trigger.dev/core/v3/build";
import { syncEnvVars } from "../core.js";

type EnvVar = { name: string; value: string; isParentEnv?: boolean };

type SupabaseBranch = {
id: string;
name: string;
project_ref: string;
parent_project_ref: string;
is_default: boolean;
git_branch: string;
status: string;
};

type SupabaseBranchDetail = {
ref: string;
db_host: string;
db_port: number;
db_user: string;
db_pass: string;
jwt_secret: string;
status: string;
};

type SupabaseApiKey = {
name: string;
api_key: string;
};

// List of Supabase related environment variables to sync
export const SUPABASE_ENV_VARS = [
"DATABASE_URL",
"POSTGRES_URL",
"SUPABASE_DB_URL",
"PGHOST",
"PGPORT",
"PGUSER",
"PGPASSWORD",
"PGDATABASE",
"SUPABASE_URL",
"SUPABASE_ANON_KEY",
"SUPABASE_SERVICE_ROLE_KEY",
"SUPABASE_JWT_SECRET",
];

function buildSupabaseEnvVarMappings(options: {
user: string;
password: string;
host: string;
port: number;
database: string;
ref: string;
jwtSecret: string;
anonKey?: string;
serviceRoleKey?: string;
}): Record<string, string> {
const { user, password, host, port, database, ref, jwtSecret, anonKey, serviceRoleKey } = options;

const connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;

const mappings: Record<string, string> = {
DATABASE_URL: connectionString,
POSTGRES_URL: connectionString,
SUPABASE_DB_URL: connectionString,
PGHOST: host,
PGPORT: String(port),
PGUSER: user,
PGPASSWORD: password,
PGDATABASE: database,
SUPABASE_URL: `https://${ref}.supabase.co`,
SUPABASE_JWT_SECRET: jwtSecret,
};

if (anonKey) {
mappings.SUPABASE_ANON_KEY = anonKey;
}

if (serviceRoleKey) {
mappings.SUPABASE_SERVICE_ROLE_KEY = serviceRoleKey;
}

return mappings;
}

export function syncSupabaseEnvVars(options?: {
projectId?: string;
/**
* Supabase Management API access token for authentication.
* It's recommended to use the SUPABASE_ACCESS_TOKEN environment variable instead of hardcoding this value.
*/
supabaseAccessToken?: string;
branch?: string;
envVarPrefix?: string;
}): BuildExtension {
const sync = syncEnvVars(async (ctx) => {
const projectId =
options?.projectId ?? process.env.SUPABASE_PROJECT_ID ?? ctx.env.SUPABASE_PROJECT_ID;
const supabaseAccessToken =
options?.supabaseAccessToken ??
process.env.SUPABASE_ACCESS_TOKEN ??
ctx.env.SUPABASE_ACCESS_TOKEN;
const branch = options?.branch ?? ctx.branch;
const envVarPrefix = options?.envVarPrefix ?? "";
const outputEnvVars = SUPABASE_ENV_VARS;

// Skip the whole process for Vercel environments
if (ctx.env.VERCEL) {
return [];
}

if (!projectId) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a projectId or set the SUPABASE_PROJECT_ID env var."
);
}

if (!supabaseAccessToken) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a supabaseAccessToken or set the SUPABASE_ACCESS_TOKEN env var."
);
}

// Skip branch-specific logic for production environment
if (ctx.environment === "prod") {
return [];
}

if (!branch) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
);
}

if (ctx.environment === "dev") {
// Skip syncing for development environment
return [];
}
Comment on lines +124 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reorder environment checks to avoid unexpected errors in dev environment.

The dev environment skip (line 135) is checked after the branch validation (line 129). If a user deploys to dev without a branch, they'll get a confusing error about missing branch instead of the expected silent skip.

Move the dev check before the branch validation to match the documented behavior.

Proposed fix
     // Skip branch-specific logic for production environment
     if (ctx.environment === "prod") {
       return [];
     }

+    if (ctx.environment === "dev") {
+      // Skip syncing for development environment
+      return [];
+    }
+
     if (!branch) {
       throw new Error(
         "syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
       );
     }
-
-    if (ctx.environment === "dev") {
-      // Skip syncing for development environment
-      return [];
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Skip branch-specific logic for production environment
if (ctx.environment === "prod") {
return [];
}
if (!branch) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
);
}
if (ctx.environment === "dev") {
// Skip syncing for development environment
return [];
}
// Skip branch-specific logic for production environment
if (ctx.environment === "prod") {
return [];
}
if (ctx.environment === "dev") {
// Skip syncing for development environment
return [];
}
if (!branch) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/build/src/extensions/core/syncSupabaseEnvVars.ts` around lines 124 -
138, The branch-missing error is raised before we skip for dev, causing false
errors when ctx.environment === "dev"; update the logic in syncSupabaseEnvVars
(the function using ctx.environment and branch) to check if ctx.environment ===
"dev" and return [] before performing the branch validation, so the branch-null
throw ("syncSupabaseEnvVars: you did not pass in a branch...") only runs for
non-dev environments.


const headers = {
Authorization: `Bearer ${supabaseAccessToken}`,
};

try {
// Step 1: List branches and find the matching one by git_branch
const branchesUrl = `https://api.supabase.com/v1/projects/${projectId}/branches`;
const branchesResponse = await fetch(branchesUrl, { headers });

if (!branchesResponse.ok) {
throw new Error(`Failed to fetch Supabase branches: ${branchesResponse.status}`);
}

const branches: SupabaseBranch[] = await branchesResponse.json();

if (branches.length === 0) {
return [];
}

const matchingBranch = branches.find(
(b) => b.git_branch === branch || b.name === branch
);

if (!matchingBranch) {
// No matching branch found
return [];
}

// Step 2: Get branch configuration (connection details)
const branchDetailUrl = `https://api.supabase.com/v1/branches/${matchingBranch.id}`;
const branchDetailResponse = await fetch(branchDetailUrl, { headers });

if (!branchDetailResponse.ok) {
throw new Error(
`Failed to fetch Supabase branch details: ${branchDetailResponse.status}`
);
}

const branchDetail: SupabaseBranchDetail = await branchDetailResponse.json();

// Step 3: Get API keys for the branch project
const apiKeysUrl = `https://api.supabase.com/v1/projects/${branchDetail.ref}/api-keys`;
const apiKeysResponse = await fetch(apiKeysUrl, { headers });

let anonKey: string | undefined;
let serviceRoleKey: string | undefined;

if (apiKeysResponse.ok) {
const apiKeys: SupabaseApiKey[] = await apiKeysResponse.json();
anonKey = apiKeys.find((k) => k.name === "anon")?.api_key;
serviceRoleKey = apiKeys.find((k) => k.name === "service_role")?.api_key;
}

// Step 4: Build environment variable mappings
const envVarMappings = buildSupabaseEnvVarMappings({
user: branchDetail.db_user,
password: branchDetail.db_pass,
host: branchDetail.db_host,
port: branchDetail.db_port,
database: "postgres",
ref: branchDetail.ref,
jwtSecret: branchDetail.jwt_secret,
anonKey,
serviceRoleKey,
});

// Build output env vars
const newEnvVars: EnvVar[] = [];

for (const supabaseEnvVar of outputEnvVars) {
const prefixedKey = `${envVarPrefix}${supabaseEnvVar}`;
if (envVarMappings[supabaseEnvVar]) {
newEnvVars.push({
name: prefixedKey,
value: envVarMappings[supabaseEnvVar],
});
}
}

return newEnvVars;
} catch (error) {
console.error("Error fetching Supabase branch environment variables:", error);
throw error;
}
});

return {
name: "SyncSupabaseEnvVarsExtension",
async onBuildComplete(context, manifest) {
await sync.onBuildComplete?.(context, manifest);
},
};
}