feat(buildExtensions): syncSupabaseEnvVars build extension (#3152)

with docs
This commit is contained in:
Oskar Otwinowski
2026-03-05 11:04:26 +01:00
committed by GitHub
parent c01332297b
commit e49ccc1226
11 changed files with 466 additions and 60 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/build": patch
---
Add syncSupabaseEnvVars to pull database connection strings and save them as trigger.dev environment variables
@@ -2,6 +2,7 @@ import { Switch } from "~/components/primitives/Switch";
import { Label } from "~/components/primitives/Label";
import { Hint } from "~/components/primitives/Hint";
import { TextLink } from "~/components/primitives/TextLink";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import {
EnvironmentIcon,
environmentFullTitle,
@@ -18,6 +19,8 @@ type BuildSettingsFieldsProps = {
atomicBuilds: EnvSlug[];
onAtomicBuildsChange: (slugs: EnvSlug[]) => void;
envVarsConfigLink?: string;
/** Slugs that should be forced off and disabled, with tooltip reason. */
disabledEnvSlugs?: Partial<Record<EnvSlug, string>>;
};
export function BuildSettingsFields({
@@ -29,7 +32,11 @@ export function BuildSettingsFields({
atomicBuilds,
onAtomicBuildsChange,
envVarsConfigLink,
disabledEnvSlugs,
}: BuildSettingsFieldsProps) {
const isSlugDisabled = (slug: EnvSlug) => !!disabledEnvSlugs?.[slug];
const enabledSlugs = availableEnvSlugs.filter((s) => !isSlugDisabled(s));
return (
<>
{/* Pull env vars before build */}
@@ -41,11 +48,11 @@ export function BuildSettingsFields({
<Switch
variant="small"
checked={
availableEnvSlugs.length > 0 &&
availableEnvSlugs.every((s) => pullEnvVarsBeforeBuild.includes(s))
enabledSlugs.length > 0 &&
enabledSlugs.every((s) => pullEnvVarsBeforeBuild.includes(s))
}
onCheckedChange={(checked) => {
onPullEnvVarsChange(checked ? [...availableEnvSlugs] : []);
onPullEnvVarsChange(checked ? [...enabledSlugs] : []);
}}
/>
)}
@@ -63,8 +70,13 @@ export function BuildSettingsFields({
<div className="flex flex-col gap-2 rounded border bg-charcoal-800 p-3">
{availableEnvSlugs.map((slug) => {
const envType = envSlugToType(slug);
return (
<div key={slug} className="flex items-center justify-between">
const disabled = isSlugDisabled(slug);
const disabledReason = disabledEnvSlugs?.[slug];
const row = (
<div
key={slug}
className={`flex items-center justify-between ${disabled ? "opacity-50" : ""}`}
>
<div className="flex items-center gap-1.5">
<EnvironmentIcon environment={{ type: envType }} className="size-4" />
<span className={`text-sm ${environmentTextClassName({ type: envType })}`}>
@@ -73,7 +85,8 @@ export function BuildSettingsFields({
</div>
<Switch
variant="small"
checked={pullEnvVarsBeforeBuild.includes(slug)}
checked={disabled ? false : pullEnvVarsBeforeBuild.includes(slug)}
disabled={disabled}
onCheckedChange={(checked) => {
onPullEnvVarsChange(
checked
@@ -84,6 +97,12 @@ export function BuildSettingsFields({
/>
</div>
);
if (disabled && disabledReason) {
return (
<SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />
);
}
return row;
})}
</div>
</div>
@@ -97,17 +116,17 @@ export function BuildSettingsFields({
<Switch
variant="small"
checked={
availableEnvSlugs.length > 0 &&
availableEnvSlugs.every(
enabledSlugs.length > 0 &&
enabledSlugs.every(
(s) => discoverEnvVars.includes(s) || !pullEnvVarsBeforeBuild.includes(s)
) &&
availableEnvSlugs.some((s) => discoverEnvVars.includes(s))
enabledSlugs.some((s) => discoverEnvVars.includes(s))
}
disabled={!availableEnvSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))}
disabled={!enabledSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))}
onCheckedChange={(checked) => {
onDiscoverEnvVarsChange(
checked
? availableEnvSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s))
? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s))
: []
);
}}
@@ -122,11 +141,13 @@ export function BuildSettingsFields({
<div className="flex flex-col gap-2 rounded border bg-charcoal-800 p-3">
{availableEnvSlugs.map((slug) => {
const envType = envSlugToType(slug);
const disabled = isSlugDisabled(slug);
const disabledReason = disabledEnvSlugs?.[slug];
const isPullDisabled = !pullEnvVarsBeforeBuild.includes(slug);
return (
const row = (
<div
key={slug}
className={`flex items-center justify-between ${isPullDisabled ? "opacity-50" : ""}`}
className={`flex items-center justify-between ${disabled || isPullDisabled ? "opacity-50" : ""}`}
>
<div className="flex items-center gap-1.5">
<EnvironmentIcon environment={{ type: envType }} className="size-4" />
@@ -136,8 +157,8 @@ export function BuildSettingsFields({
</div>
<Switch
variant="small"
checked={discoverEnvVars.includes(slug)}
disabled={isPullDisabled}
checked={disabled ? false : discoverEnvVars.includes(slug)}
disabled={disabled || isPullDisabled}
onCheckedChange={(checked) => {
onDiscoverEnvVarsChange(
checked
@@ -148,6 +169,12 @@ export function BuildSettingsFields({
/>
</div>
);
if (disabled && disabledReason) {
return (
<SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />
);
}
return row;
})}
</div>
</div>
@@ -48,6 +48,7 @@ import { vercelAppInstallPath, v3ProjectSettingsIntegrationsPath, githubAppInsta
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
import { useEffect, useState, useCallback, useRef } from "react";
import { usePostHogTracking } from "~/hooks/usePostHog";
import { TextLink } from "../primitives/TextLink";
function safeRedirectUrl(url: string): string | null {
try {
@@ -224,9 +225,10 @@ export function VercelOnboardingModal({
() => availableEnvSlugsForOnboardingBuildSettings
);
// Sync pullEnvVarsBeforeBuild and discoverEnvVars when hasStagingEnvironment becomes true (once)
// Sync pullEnvVarsBeforeBuild and discoverEnvVars when hasStagingEnvironment becomes true
// AND a custom Vercel environment is mapped (once)
useEffect(() => {
if (hasStagingEnvironment && !hasSyncedStagingRef.current) {
if (hasStagingEnvironment && vercelStagingEnvironment && !hasSyncedStagingRef.current) {
hasSyncedStagingRef.current = true;
setPullEnvVarsBeforeBuild((prev) => {
if (!prev.includes("stg")) {
@@ -241,7 +243,15 @@ export function VercelOnboardingModal({
return prev;
});
}
}, [hasStagingEnvironment]);
}, [hasStagingEnvironment, vercelStagingEnvironment]);
// Strip "stg" from build settings when the staging environment mapping is cleared
useEffect(() => {
if (!vercelStagingEnvironment) {
setPullEnvVarsBeforeBuild((prev) => prev.filter((s) => s !== "stg"));
setDiscoverEnvVars((prev) => prev.filter((s) => s !== "stg"));
}
}, [vercelStagingEnvironment]);
// Sync pullEnvVarsBeforeBuild and discoverEnvVars when hasPreviewEnvironment becomes true (once)
useEffect(() => {
@@ -670,6 +680,11 @@ export function VercelOnboardingModal({
const showBuildSettings = state === "build-settings";
const showGitHubConnection = state === "github-connection";
const disabledEnvSlugsForBuildSettings =
hasStagingEnvironment && !vercelStagingEnvironment
? ({ stg: "Map a custom Vercel environment to Staging to enable this" } as Partial<Record<EnvSlug, string>>)
: undefined;
return (
<Dialog open={isOpen} onOpenChange={(open) => {
if (!open && !fromMarketplaceContext) {
@@ -731,7 +746,7 @@ export function VercelOnboardingModal({
)}
<Hint>
Once connected, your <code className="text-xs">TRIGGER_SECRET_KEY</code> will be
Once connected, your <code className="text-xs rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">TRIGGER_SECRET_KEY</code> will be
automatically synced to Vercel for each environment.
</Hint>
@@ -775,6 +790,10 @@ export function VercelOnboardingModal({
<Paragraph className="text-sm">
Select which custom Vercel environment should map to Trigger.dev's Staging
environment. Production and Preview environments are mapped automatically.
If you skip this step, the{" "}
<code className="rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">TRIGGER_SECRET_KEY</code>{" "}
will not be installed for the staging environment in Vercel. You can configure this later in
project settings.
</Paragraph>
<Select
@@ -800,15 +819,6 @@ export function VercelOnboardingModal({
))}
</Select>
<Callout variant="info">
<p className="text-xs">
If you skip this step, the{" "}
<code className="rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">TRIGGER_SECRET_KEY</code>{" "}
will not be installed for the staging environment in Vercel. You can configure this later in
project settings.
</p>
</Callout>
<Paragraph className="text-xs text-text-dimmed">
Make sure the staging branch in your Vercel project's Git settings matches the staging branch
configured in your GitHub integration.
@@ -845,25 +855,13 @@ export function VercelOnboardingModal({
{showEnvVarSync && (
<div className="flex flex-col gap-4">
<Header3>Pull Environment Variables</Header3>
<Paragraph className="text-sm">
Select which environment variables to pull from Vercel now. This is a one-time pull.
Later on environment variables can be pulled before each build.
</Paragraph>
<div className="flex gap-4 text-sm">
<div className="rounded border bg-charcoal-750 px-3 py-2">
<span className="font-medium text-text-bright">{syncableEnvVars.length}</span>
<span className="text-text-dimmed"> can be pulled</span>
</div>
{secretEnvVars.length > 0 && (
<div className="rounded border bg-charcoal-750 px-3 py-2">
<span className="font-medium text-amber-400">{secretEnvVars.length}</span>
<span className="text-text-dimmed"> secret (cannot pull)</span>
</div>
)}
<div className="flex flex-col gap-1">
<Header3>Pull Environment Variables</Header3>
<Paragraph className="text-sm">
Choose which environment variables to import from Vercel. This runs as a one time pull to prefill your project with the variables it needs. Youll be able to pull again later, or enable automatic syncing before each build if you prefer.
If you are using Supabase or Neon branching, <TextLink href="https://trigger.dev/docs/vercel-integration#supabase-and-neon-database-branching" target="_blank" rel="noopener noreferrer">read the docs</TextLink> for the recommended setup.
</Paragraph>
</div>
<div className="flex items-center justify-between rounded border bg-charcoal-800 p-3">
<div>
<Label>Pull all environment variables now</Label>
@@ -1052,6 +1050,7 @@ export function VercelOnboardingModal({
onDiscoverEnvVarsChange={setDiscoverEnvVars}
atomicBuilds={atomicBuilds}
onAtomicBuildsChange={setAtomicBuilds}
disabledEnvSlugs={disabledEnvSlugsForBuildSettings}
/>
<FormButtons
@@ -1759,7 +1759,7 @@ export class VercelIntegrationRepository {
vercelProjectId: string,
teamId?: string | null
): ResultAsync<void, VercelApiError> {
return wrapVercelCall(
return wrapVercelCallWithRecovery(
client.projects.updateProject({
idOrName: vercelProjectId,
...(teamId && { teamId }),
@@ -1767,8 +1767,10 @@ export class VercelIntegrationRepository {
autoAssignCustomDomains: false,
},
}),
VercelSchemas.updateProject,
"Failed to disable autoAssignCustomDomains",
{ vercelProjectId, teamId }
{ vercelProjectId, teamId },
toVercelApiError
).map(() => undefined);
}
@@ -632,6 +632,11 @@ function ConnectedVercelProjectForm({
const availableEnvSlugs = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment);
const availableEnvSlugsForBuildSettings = getAvailableEnvSlugsForBuildSettings(hasStagingEnvironment, hasPreviewEnvironment);
const disabledEnvSlugsForBuildSettings: Partial<Record<EnvSlug, string>> | undefined =
hasStagingEnvironment && !configValues.vercelStagingEnvironment
? { stg: "Map a custom Vercel environment to Staging to enable this" }
: undefined;
const formatSelectedEnvs = (selected: EnvSlug[], availableSlugs: EnvSlug[] = availableEnvSlugs): string => {
if (selected.length === 0) return "None selected";
if (selected.length === availableSlugs.length) return "All environments";
@@ -727,12 +732,24 @@ function ConnectedVercelProjectForm({
setValue={(value) => {
if (!Array.isArray(value)) {
const env = customEnvironments?.find((e) => e.id === value);
setConfigValues((prev) => ({
...prev,
vercelStagingEnvironment: env
? { environmentId: env.id, displayName: env.slug }
: null,
}));
setConfigValues((prev) => {
const next = {
...prev,
vercelStagingEnvironment: env
? { environmentId: env.id, displayName: env.slug }
: null,
};
// When clearing the staging mapping, strip "stg" from build settings
if (!env) {
next.pullEnvVarsBeforeBuild = prev.pullEnvVarsBeforeBuild.filter(
(s) => s !== "stg"
);
next.discoverEnvVars = prev.discoverEnvVars.filter(
(s) => s !== "stg"
);
}
return next;
});
}
}}
items={[{ id: "", slug: "None" }, ...customEnvironments]}
@@ -770,6 +787,7 @@ function ConnectedVercelProjectForm({
setConfigValues((prev) => ({ ...prev, atomicBuilds: slugs }))
}
envVarsConfigLink={`/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/environment-variables`}
disabledEnvSlugs={disabledEnvSlugsForBuildSettings}
/>
{/* Warning: autoAssignCustomDomains must be disabled for atomic deployments */}
@@ -85,8 +85,8 @@ export function createDefaultVercelIntegrationData(
return {
config: {
atomicBuilds: ["prod"],
pullEnvVarsBeforeBuild: ["prod", "stg", "preview"],
discoverEnvVars: ["prod", "stg", "preview"],
pullEnvVarsBeforeBuild: ["prod", "preview"],
discoverEnvVars: ["prod", "preview"],
vercelStagingEnvironment: null,
},
syncEnvVarsMapping: {},
+1
View File
@@ -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 |
+69
View File
@@ -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>
For `prod` environments, this extension uses credentials from your default Supabase
branch. For `preview` and `staging` environments, it matches the git branch name to a Supabase
branch and syncs the corresponding database connection strings and API keys. `dev` environments are skipped.
</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
+3 -4
View File
@@ -104,10 +104,9 @@ The following variables are excluded from the Vercel → Trigger.dev sync:
You can control sync behavior per-variable from your project's Vercel settings. Deselecting a variable prevents its value from being updated during future syncs.
<Tip>
For dynamic environment variables (e.g., from NeonDB branching), use the `syncEnvVars` build
extension instead. Learn more about [environment variables](/deploy-environment-variables).
</Tip>
### Supabase and Neon database branching
If you use [Supabase Branching](https://supabase.com/docs/guides/deployment/branching) or [Neon Database Branching](https://neon.tech/docs/guides/branching-intro) for preview environments, disable syncing for database env vars on the Environment Variables page and use the [syncSupabaseEnvVars](/config/extensions/syncEnvVars#syncsupabaseenvvars) or [syncNeonEnvVars](/config/extensions/syncEnvVars#syncneonenvvars) build extensions instead. These extensions automatically resolve the correct branch-specific credentials at build time.
## Atomic deployments
+1
View File
@@ -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";
@@ -0,0 +1,285 @@
import { tryCatch } from "@trigger.dev/core/v3";
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}?sslmode=require`;
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) => {
// Skip for development environments
if (ctx.environment === "dev") {
return [];
}
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."
);
}
const headers = {
Authorization: `Bearer ${supabaseAccessToken}`,
};
// Step 1: List branches
const branchesUrl = `https://api.supabase.com/v1/projects/${projectId}/branches`;
const [branchesFetchError, branchesResponse] = await tryCatch(
fetch(branchesUrl, { headers })
);
if (branchesFetchError) {
throw new Error(
`syncSupabaseEnvVars: network error fetching branches from ${branchesUrl}: ${branchesFetchError.message}`
);
}
if (!branchesResponse.ok) {
throw new Error(
`syncSupabaseEnvVars: failed to list branches from ${branchesUrl} (status ${branchesResponse.status})`
);
}
const [branchesParseError, branches] = await tryCatch(
branchesResponse.json() as Promise<SupabaseBranch[]>
);
if (branchesParseError) {
throw new Error(
`syncSupabaseEnvVars: failed to parse branches response from ${branchesUrl}: ${branchesParseError.message}`
);
}
if (branches.length === 0) {
return [];
}
// Step 2: Find the target branch based on environment
let targetBranch: SupabaseBranch | undefined;
if (ctx.environment === "prod") {
targetBranch = branches.find((b) => b.is_default);
if (!targetBranch) {
throw new Error(
"syncSupabaseEnvVars: no default Supabase branch found for the project."
);
}
} else {
if (!branch) {
throw new Error(
"syncSupabaseEnvVars: you did not pass in a branch and no branch was detected from context."
);
}
targetBranch = branches.find((b) => b.git_branch === branch || b.name === branch);
if (!targetBranch) {
return [];
}
}
// Step 3: Get branch configuration (connection details)
const branchDetailUrl = `https://api.supabase.com/v1/branches/${targetBranch.id}`;
const [detailFetchError, branchDetailResponse] = await tryCatch(
fetch(branchDetailUrl, { headers })
);
if (detailFetchError) {
throw new Error(
`syncSupabaseEnvVars: network error fetching branch details from ${branchDetailUrl}: ${detailFetchError.message}`
);
}
if (!branchDetailResponse.ok) {
throw new Error(
`syncSupabaseEnvVars: failed to fetch branch details from ${branchDetailUrl} (status ${branchDetailResponse.status})`
);
}
const [detailParseError, branchDetail] = await tryCatch(
branchDetailResponse.json() as Promise<SupabaseBranchDetail>
);
if (detailParseError) {
throw new Error(
`syncSupabaseEnvVars: failed to parse branch details response from ${branchDetailUrl}: ${detailParseError.message}`
);
}
// Step 4: Get API keys for the branch project
const apiKeysUrl = `https://api.supabase.com/v1/projects/${branchDetail.ref}/api-keys`;
const [apiKeysFetchError, apiKeysResponse] = await tryCatch(
fetch(apiKeysUrl, { headers })
);
let anonKey: string | undefined;
let serviceRoleKey: string | undefined;
if (apiKeysFetchError) {
console.warn(
`syncSupabaseEnvVars: failed to fetch API keys from ${apiKeysUrl}: ${apiKeysFetchError.message}`
);
} else if (!apiKeysResponse.ok) {
console.warn(
`syncSupabaseEnvVars: failed to fetch API keys from ${apiKeysUrl} (status ${apiKeysResponse.status})`
);
} else {
const [apiKeysParseError, apiKeys] = await tryCatch(
apiKeysResponse.json() as Promise<SupabaseApiKey[]>
);
if (apiKeysParseError) {
console.warn(
`syncSupabaseEnvVars: failed to parse API keys response from ${apiKeysUrl}: ${apiKeysParseError.message}`
);
} else {
anonKey = apiKeys.find((k) => k.name === "anon")?.api_key;
serviceRoleKey = apiKeys.find((k) => k.name === "service_role")?.api_key;
}
}
// Step 5: 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,
});
const newEnvVars: EnvVar[] = [];
for (const supabaseEnvVar of outputEnvVars) {
const prefixedKey = `${envVarPrefix}${supabaseEnvVar}`;
if (envVarMappings[supabaseEnvVar]) {
newEnvVars.push({
name: prefixedKey,
value: envVarMappings[supabaseEnvVar],
});
}
}
return newEnvVars;
});
return {
name: "SyncSupabaseEnvVarsExtension",
async onBuildComplete(context, manifest) {
await sync.onBuildComplete?.(context, manifest);
},
};
}