Skip to content

Commit 7f391fc

Browse files
committed
feat(buildExtensions): syncSupabaseEnvVars build extension
1 parent 24b92d3 commit 7f391fc

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

docs/config/extensions/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Trigger.dev provides a set of built-in extensions that you can use to customize
5757
| [additionalPackages](/config/extensions/additionalPackages) | Install additional npm packages in your build image |
5858
| [syncEnvVars](/config/extensions/syncEnvVars) | Automatically sync environment variables from external services to Trigger.dev |
5959
| [syncVercelEnvVars](/config/extensions/syncEnvVars#syncVercelEnvVars) | Automatically sync environment variables from Vercel to Trigger.dev |
60+
| [syncSupabaseEnvVars](/config/extensions/syncEnvVars#syncSupabaseEnvVars) | Automatically sync environment variables from Supabase to Trigger.dev |
6061
| [esbuildPlugin](/config/extensions/esbuildPlugin) | Add existing or custom esbuild extensions to customize your build process |
6162
| [emitDecoratorMetadata](/config/extensions/emitDecoratorMetadata) | Enable `emitDecoratorMetadata` in your TypeScript build |
6263
| [audioWaveform](/config/extensions/audioWaveform) | Add Audio Waveform to your build image |

docs/config/extensions/syncEnvVars.mdx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,72 @@ The extension syncs the following environment variables (with optional prefix):
220220
- `POSTGRES_PRISMA_URL` - Connection string optimized for Prisma
221221
- `POSTGRES_HOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`
222222
- `PGHOST`, `PGHOST_UNPOOLED`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`
223+
224+
### syncSupabaseEnvVars
225+
226+
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.
227+
228+
<AccordionGroup>
229+
<Accordion title="Setting up authentication">
230+
You need to set the `SUPABASE_ACCESS_TOKEN` and `SUPABASE_PROJECT_ID` environment variables, or pass them
231+
as arguments to the `syncSupabaseEnvVars` build extension.
232+
233+
You can generate a `SUPABASE_ACCESS_TOKEN` in your Supabase [dashboard](https://supabase.com/dashboard/account/tokens).
234+
</Accordion>
235+
236+
<Accordion title="Running in Vercel environment">
237+
When running the build from a Vercel environment (determined by checking if the `VERCEL`
238+
environment variable is present), this extension is skipped entirely.
239+
</Accordion>
240+
</AccordionGroup>
241+
242+
<Note>
243+
This extension is skipped for `prod` and `dev` environments. It is designed to sync
244+
branch-specific database connections and API keys for preview/staging environments using Supabase
245+
Branching.
246+
</Note>
247+
248+
```ts
249+
import { defineConfig } from "@trigger.dev/sdk";
250+
import { syncSupabaseEnvVars } from "@trigger.dev/build/extensions/core";
251+
252+
export default defineConfig({
253+
project: "<project ref>",
254+
// Your other config settings...
255+
build: {
256+
// This will automatically use the SUPABASE_ACCESS_TOKEN and SUPABASE_PROJECT_ID environment variables
257+
extensions: [syncSupabaseEnvVars()],
258+
},
259+
});
260+
```
261+
262+
Or you can pass in the token, project ID, and other options as arguments:
263+
264+
```ts
265+
import { defineConfig } from "@trigger.dev/sdk";
266+
import { syncSupabaseEnvVars } from "@trigger.dev/build/extensions/core";
267+
268+
export default defineConfig({
269+
project: "<project ref>",
270+
// Your other config settings...
271+
build: {
272+
extensions: [
273+
syncSupabaseEnvVars({
274+
projectId: "your-supabase-project-id",
275+
supabaseAccessToken: "your-supabase-access-token", // optional, we recommend to keep it as env variable
276+
branch: "your-branch-name", // optional, defaults to ctx.branch
277+
envVarPrefix: "MY_PREFIX_", // optional, prefix for all synced env vars
278+
}),
279+
],
280+
},
281+
});
282+
```
283+
284+
The extension syncs the following environment variables (with optional prefix):
285+
286+
- `DATABASE_URL`, `POSTGRES_URL`, `SUPABASE_DB_URL` — PostgreSQL connection strings
287+
- `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` — Individual connection parameters
288+
- `SUPABASE_URL` — Supabase API URL
289+
- `SUPABASE_ANON_KEY` — Anonymous API key
290+
- `SUPABASE_SERVICE_ROLE_KEY` — Service role API key
291+
- `SUPABASE_JWT_SECRET` — JWT secret

packages/build/src/extensions/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "./core/aptGet.js";
55
export * from "./core/ffmpeg.js";
66
export * from "./core/neonSyncEnvVars.js";
77
export * from "./core/vercelSyncEnvVars.js";
8+
export * from "./core/syncSupabaseEnvVars.js";
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { BuildExtension } from "@trigger.dev/core/v3/build";
2+
import { syncEnvVars } from "../core.js";
3+
4+
type EnvVar = { name: string; value: string; isParentEnv?: boolean };
5+
6+
type SupabaseBranch = {
7+
id: string;
8+
name: string;
9+
project_ref: string;
10+
parent_project_ref: string;
11+
is_default: boolean;
12+
git_branch: string;
13+
status: string;
14+
};
15+
16+
type SupabaseBranchDetail = {
17+
ref: string;
18+
db_host: string;
19+
db_port: number;
20+
db_user: string;
21+
db_pass: string;
22+
jwt_secret: string;
23+
status: string;
24+
};
25+
26+
type SupabaseApiKey = {
27+
name: string;
28+
api_key: string;
29+
};
30+
31+
// List of Supabase related environment variables to sync
32+
export const SUPABASE_ENV_VARS = [
33+
"DATABASE_URL",
34+
"POSTGRES_URL",
35+
"SUPABASE_DB_URL",
36+
"PGHOST",
37+
"PGPORT",
38+
"PGUSER",
39+
"PGPASSWORD",
40+
"PGDATABASE",
41+
"SUPABASE_URL",
42+
"SUPABASE_ANON_KEY",
43+
"SUPABASE_SERVICE_ROLE_KEY",
44+
"SUPABASE_JWT_SECRET",
45+
];
46+
47+
function buildSupabaseEnvVarMappings(options: {
48+
user: string;
49+
password: string;
50+
host: string;
51+
port: number;
52+
database: string;
53+
ref: string;
54+
jwtSecret: string;
55+
anonKey?: string;
56+
serviceRoleKey?: string;
57+
}): Record<string, string> {
58+
const { user, password, host, port, database, ref, jwtSecret, anonKey, serviceRoleKey } = options;
59+
60+
const connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
61+
62+
const mappings: Record<string, string> = {
63+
DATABASE_URL: connectionString,
64+
POSTGRES_URL: connectionString,
65+
SUPABASE_DB_URL: connectionString,
66+
PGHOST: host,
67+
PGPORT: String(port),
68+
PGUSER: user,
69+
PGPASSWORD: password,
70+
PGDATABASE: database,
71+
SUPABASE_URL: `https://${ref}.supabase.co`,
72+
SUPABASE_JWT_SECRET: jwtSecret,
73+
};
74+
75+
if (anonKey) {
76+
mappings.SUPABASE_ANON_KEY = anonKey;
77+
}
78+
79+
if (serviceRoleKey) {
80+
mappings.SUPABASE_SERVICE_ROLE_KEY = serviceRoleKey;
81+
}
82+
83+
return mappings;
84+
}
85+
86+
export function syncSupabaseEnvVars(options?: {
87+
projectId?: string;
88+
/**
89+
* Supabase Management API access token for authentication.
90+
* It's recommended to use the SUPABASE_ACCESS_TOKEN environment variable instead of hardcoding this value.
91+
*/
92+
supabaseAccessToken?: string;
93+
branch?: string;
94+
envVarPrefix?: string;
95+
}): BuildExtension {
96+
const sync = syncEnvVars(async (ctx) => {
97+
const projectId =
98+
options?.projectId ?? process.env.SUPABASE_PROJECT_ID ?? ctx.env.SUPABASE_PROJECT_ID;
99+
const supabaseAccessToken =
100+
options?.supabaseAccessToken ??
101+
process.env.SUPABASE_ACCESS_TOKEN ??
102+
ctx.env.SUPABASE_ACCESS_TOKEN;
103+
const branch = options?.branch ?? ctx.branch;
104+
const envVarPrefix = options?.envVarPrefix ?? "";
105+
const outputEnvVars = SUPABASE_ENV_VARS;
106+
107+
// Skip the whole process for Vercel environments
108+
if (ctx.env.VERCEL) {
109+
return [];
110+
}
111+
112+
if (!projectId) {
113+
throw new Error(
114+
"syncSupabaseEnvVars: you did not pass in a projectId or set the SUPABASE_PROJECT_ID env var."
115+
);
116+
}
117+
118+
if (!supabaseAccessToken) {
119+
throw new Error(
120+
"syncSupabaseEnvVars: you did not pass in a supabaseAccessToken or set the SUPABASE_ACCESS_TOKEN env var."
121+
);
122+
}
123+
124+
// Skip branch-specific logic for production environment
125+
if (ctx.environment === "prod") {
126+
return [];
127+
}
128+
129+
if (!branch) {
130+
throw new Error(
131+
"syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
132+
);
133+
}
134+
135+
if (ctx.environment === "dev") {
136+
// Skip syncing for development environment
137+
return [];
138+
}
139+
140+
const headers = {
141+
Authorization: `Bearer ${supabaseAccessToken}`,
142+
};
143+
144+
try {
145+
// Step 1: List branches and find the matching one by git_branch
146+
const branchesUrl = `https://api.supabase.com/v1/projects/${projectId}/branches`;
147+
const branchesResponse = await fetch(branchesUrl, { headers });
148+
149+
if (!branchesResponse.ok) {
150+
throw new Error(`Failed to fetch Supabase branches: ${branchesResponse.status}`);
151+
}
152+
153+
const branches: SupabaseBranch[] = await branchesResponse.json();
154+
155+
if (branches.length === 0) {
156+
return [];
157+
}
158+
159+
const matchingBranch = branches.find(
160+
(b) => b.git_branch === branch || b.name === branch
161+
);
162+
163+
if (!matchingBranch) {
164+
// No matching branch found
165+
return [];
166+
}
167+
168+
// Step 2: Get branch configuration (connection details)
169+
const branchDetailUrl = `https://api.supabase.com/v1/branches/${matchingBranch.id}`;
170+
const branchDetailResponse = await fetch(branchDetailUrl, { headers });
171+
172+
if (!branchDetailResponse.ok) {
173+
throw new Error(
174+
`Failed to fetch Supabase branch details: ${branchDetailResponse.status}`
175+
);
176+
}
177+
178+
const branchDetail: SupabaseBranchDetail = await branchDetailResponse.json();
179+
180+
// Step 3: Get API keys for the branch project
181+
const apiKeysUrl = `https://api.supabase.com/v1/projects/${branchDetail.ref}/api-keys`;
182+
const apiKeysResponse = await fetch(apiKeysUrl, { headers });
183+
184+
let anonKey: string | undefined;
185+
let serviceRoleKey: string | undefined;
186+
187+
if (apiKeysResponse.ok) {
188+
const apiKeys: SupabaseApiKey[] = await apiKeysResponse.json();
189+
anonKey = apiKeys.find((k) => k.name === "anon")?.api_key;
190+
serviceRoleKey = apiKeys.find((k) => k.name === "service_role")?.api_key;
191+
}
192+
193+
// Step 4: Build environment variable mappings
194+
const envVarMappings = buildSupabaseEnvVarMappings({
195+
user: branchDetail.db_user,
196+
password: branchDetail.db_pass,
197+
host: branchDetail.db_host,
198+
port: branchDetail.db_port,
199+
database: "postgres",
200+
ref: branchDetail.ref,
201+
jwtSecret: branchDetail.jwt_secret,
202+
anonKey,
203+
serviceRoleKey,
204+
});
205+
206+
// Build output env vars
207+
const newEnvVars: EnvVar[] = [];
208+
209+
for (const supabaseEnvVar of outputEnvVars) {
210+
const prefixedKey = `${envVarPrefix}${supabaseEnvVar}`;
211+
if (envVarMappings[supabaseEnvVar]) {
212+
newEnvVars.push({
213+
name: prefixedKey,
214+
value: envVarMappings[supabaseEnvVar],
215+
});
216+
}
217+
}
218+
219+
return newEnvVars;
220+
} catch (error) {
221+
console.error("Error fetching Supabase branch environment variables:", error);
222+
throw error;
223+
}
224+
});
225+
226+
return {
227+
name: "SyncSupabaseEnvVarsExtension",
228+
async onBuildComplete(context, manifest) {
229+
await sync.onBuildComplete?.(context, manifest);
230+
},
231+
};
232+
}

0 commit comments

Comments
 (0)