Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/env-pull-next.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": patch
---

Default `clerk env pull` to `.env.local` on Next.js projects with no existing env file, matching the framework's convention for local secrets. Projects that already have keys in `.env` continue to write there.
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/env/pull.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,11 +500,11 @@ describe("env pull", () => {

await runEnvPull();

// Next.js prefers .env (gitignored by create-next-app via .env* pattern)
const content = await Bun.file(join(tempDir, ".env")).text();
// Next.js prefers .env.local (always gitignored, per Next.js convention for secrets)
const content = await Bun.file(join(tempDir, ".env.local")).text();
expect(content).toContain("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_abc123");
// Should NOT have created .env.local
expect(await Bun.file(join(tempDir, ".env.local")).exists()).toBe(false);
// Should NOT have created .env
expect(await Bun.file(join(tempDir, ".env")).exists()).toBe(false);
});

test("Next.js writes to existing .env.local if it already has Clerk keys", async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/init/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,15 @@ test("Next.js: uses .env when only .env exists", async () => {
expect(ctx!.envFile).toBe(".env");
});

test("Next.js: uses .env when neither .env nor .env.local exists", async () => {
test("Next.js: uses .env.local when neither .env nor .env.local exists", async () => {
await Bun.write(
join(tempDir, "package.json"),
JSON.stringify({ dependencies: { next: "15.0.0" } }),
);

const ctx = await gatherContext(tempDir);

expect(ctx!.envFile).toBe(".env");
expect(ctx!.envFile).toBe(".env.local");
});

test("Next.js: uses .env.local when both .env and .env.local exist (keep existing setup)", async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,14 +650,14 @@ describe("init", () => {
name: "Next.js",
sdk: "@clerk/nextjs",
envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
envFile: ".env" as const,
envFile: ".env.local" as const,
},
typescript: true,
srcDir: false,
packageManager: "npm" as const,
existingClerk: false,
deps: { next: "15.0.0" },
envFile: ".env",
envFile: ".env.local",
};

gatherContextSpy.mockResolvedValue(mockCtx);
Expand All @@ -668,7 +668,7 @@ describe("init", () => {

await init({ yes: true });

expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: mockCtx.cwd });
expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env.local", cwd: mockCtx.cwd });
});

test("bootstrap passes project dir to link, not parent cwd", async () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/cli-core/src/lib/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export interface FrameworkInfo {
envVar: string;
/** Override for secret key env var name. Defaults to CLERK_SECRET_KEY when omitted. */
secretKeyEnvVar?: string;
/** Preferred env file for secrets. Frameworks that gitignore `.env` use it
* directly; Vite-based frameworks use `.env.local` since `.env` is tracked. */
/** Preferred env file for secrets when the project has none yet. Frameworks
* with a `.env.local` convention use it (always gitignored, per-machine
* overrides); frameworks without that convention fall back to `.env`. */
envFile: ".env" | ".env.local";
/** When true, the framework's Clerk SDK supports keyless mode (auto-generated
* temporary dev keys). Frameworks without keyless support require API keys
Expand All @@ -31,7 +32,7 @@ export const FRAMEWORK_MAP: FrameworkInfo[] = [
name: "Next.js",
sdk: "@clerk/nextjs",
envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
envFile: ".env",
envFile: ".env.local",
supportsKeyless: true,
},
{
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/lib/keyless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ describe("writeKeysToEnvFile", () => {
}),
);

// Next.js declares envFile: ".env" in FRAMEWORK_MAP
const content = await Bun.file(join(tempDir, ".env")).text();
// Next.js declares envFile: ".env.local" in FRAMEWORK_MAP
const content = await Bun.file(join(tempDir, ".env.local")).text();
expect(content).toContain("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_next");
expect(content).toContain("CLERK_SECRET_KEY=sk_test_next");
});
Expand Down
7 changes: 5 additions & 2 deletions packages/cli-core/src/test/integration/deploy-to-prod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test.each([{ mode: "human" }, { mode: "agent" }])(

// Pull dev env (default)
await clerk("--mode", mode, "env", "pull");
const devEnv = parseEnvFile(await Bun.file(join(h.tempDir, ".env")).text(), ".env");
const devEnv = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local");
expect(devEnv.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(devInstance.publishable_key);
expect(devEnv.get("CLERK_SECRET_KEY")).toBe(devInstance.secret_key);

Expand All @@ -53,7 +53,10 @@ test.each([{ mode: "human" }, { mode: "agent" }])(
expect(prodEnv.get("CLERK_SECRET_KEY")).toBe(prodInstance.secret_key);

// Dev file not overwritten
const devEnvAfter = parseEnvFile(await Bun.file(join(h.tempDir, ".env")).text(), ".env");
const devEnvAfter = parseEnvFile(
await Bun.file(join(h.tempDir, ".env.local")).text(),
".env.local",
);
expect(devEnvAfter.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(devInstance.publishable_key);

// Config pull targets prod instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe("Recover from errors gracefully", () => {
[`/applications/${MOCK_APP.application_id}`]: MOCK_APP,
});
await clerk("--mode", "human", "env", "pull");
const env = parseEnvFile(await Bun.file(join(h.tempDir, ".env")).text(), ".env");
const env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local");
expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(devInstance.publishable_key);
expect(env.get("CLERK_SECRET_KEY")).toBe(devInstance.secret_key);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/test/integration/onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.each([
framework: "Next.js",
deps: { next: "15.0.0" },
expectedKey: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
envFile: ".env",
envFile: ".env.local",
},
{
framework: "React/Vite",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/test/integration/switch-apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test("re-link from one app to another", async () => {

// Pull env for App A
await clerk("--mode", "human", "env", "pull");
let env = parseEnvFile(await Bun.file(join(h.tempDir, ".env")).text(), ".env");
let env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local");
expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(appADev.publishable_key);

// Unlink
Expand All @@ -61,7 +61,7 @@ test("re-link from one app to another", async () => {

// Pull env for App B — should overwrite App A's values, not append
await clerk("--mode", "human", "env", "pull");
env = parseEnvFile(await Bun.file(join(h.tempDir, ".env")).text(), ".env");
env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local");
expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(appBDev.publishable_key);
expect(env.get("CLERK_SECRET_KEY")).toBe(appBDev.secret_key);
});
Loading