Skip to content

Commit 6d980c8

Browse files
committed
fix: lazy-init server drivers to fix build without env vars
SvelteKit's static analysis imports server modules during build, causing env validation to fail when DATABASE_URL etc. aren't set. Changed env, db, s3, and auth to use lazy initialization via Proxy.
1 parent 80a8d2d commit 6d980c8

6 files changed

Lines changed: 135 additions & 51 deletions

File tree

src/lib/env/env.server.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ describe("env production guard", () => {
3131
process.env.UNSAFE_DISABLE_AUTH = "true";
3232

3333
expect(() => {
34-
// Force re-import to trigger validation
34+
// Force re-import to clear cache and trigger fresh validation
3535
delete require.cache[require.resolve("./env.server.ts")];
36-
require("./env.server.ts");
36+
const { env } = require("./env.server.ts");
37+
// Access env to trigger lazy evaluation
38+
void env.DATABASE_URL;
3739
}).toThrow("UNSAFE_DISABLE_AUTH cannot be enabled in production");
3840
});
3941

@@ -43,7 +45,8 @@ describe("env production guard", () => {
4345

4446
expect(() => {
4547
delete require.cache[require.resolve("./env.server.ts")];
46-
require("./env.server.ts");
48+
const { env } = require("./env.server.ts");
49+
void env.DATABASE_URL;
4750
}).not.toThrow();
4851
});
4952

@@ -53,7 +56,8 @@ describe("env production guard", () => {
5356

5457
expect(() => {
5558
delete require.cache[require.resolve("./env.server.ts")];
56-
require("./env.server.ts");
59+
const { env } = require("./env.server.ts");
60+
void env.DATABASE_URL;
5761
}).not.toThrow();
5862
});
5963
});

src/lib/env/env.server.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,26 @@ const Env = v.object({
1818
CLOUDFLARE_API_TOKEN: v.optional(v.string()),
1919
});
2020

21-
export const env = v.parse(Env, process.env);
21+
type EnvType = v.InferOutput<typeof Env>;
2222

23-
// Production guard: UNSAFE_DISABLE_AUTH must never be enabled in production
24-
if (env.UNSAFE_DISABLE_AUTH === "true" && process.env.NODE_ENV === "production") {
25-
throw new Error("UNSAFE_DISABLE_AUTH cannot be enabled in production");
23+
let _env: EnvType | null = null;
24+
25+
function getEnv(): EnvType {
26+
if (_env) return _env;
27+
28+
_env = v.parse(Env, process.env);
29+
30+
// Production guard: UNSAFE_DISABLE_AUTH must never be enabled in production
31+
if (_env.UNSAFE_DISABLE_AUTH === "true" && process.env.NODE_ENV === "production") {
32+
throw new Error("UNSAFE_DISABLE_AUTH cannot be enabled in production");
33+
}
34+
35+
return _env;
2636
}
37+
38+
// Lazy evaluation via Proxy - env is only parsed when accessed at runtime
39+
export const env: EnvType = new Proxy({} as EnvType, {
40+
get(_, prop: string) {
41+
return getEnv()[prop as keyof EnvType];
42+
},
43+
});

src/lib/server/database/storage.server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { env } from "$lib/env/env.server";
2-
import { bucket, ensureBucket, s3 } from "$lib/server/drivers/s3";
2+
import { ensureBucket, getBucket, s3 } from "$lib/server/drivers/s3";
33

44
export async function uploadBuffer(
55
buffer: Buffer,
@@ -11,7 +11,7 @@ export async function uploadBuffer(
1111

1212
const key = `${path}/${crypto.randomUUID()}-${fileName}`;
1313

14-
await s3.putObject(bucket, key, buffer, buffer.length, {
14+
await s3.putObject(getBucket(), key, buffer, buffer.length, {
1515
"Content-Type": contentType,
1616
});
1717

@@ -20,11 +20,11 @@ export async function uploadBuffer(
2020
}
2121

2222
export async function deleteFile(key: string): Promise<void> {
23-
await s3.removeObject(bucket, key);
23+
await s3.removeObject(getBucket(), key);
2424
}
2525

2626
export async function listFiles(prefix: string): Promise<string[]> {
27-
const stream = s3.listObjects(bucket, prefix, true);
27+
const stream = s3.listObjects(getBucket(), prefix, true);
2828
const keys: string[] = [];
2929

3030
return new Promise((resolve, reject) => {

src/lib/server/drivers/auth.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,40 @@ import { env } from "$lib/env/env.server";
44
import { db } from "$lib/server/drivers/db";
55
import * as schema from "$lib/shared/models/schema";
66

7-
export const auth = betterAuth({
8-
database: drizzleAdapter(db, {
9-
provider: "pg",
10-
schema,
11-
}),
12-
baseURL: env.BETTER_AUTH_URL,
13-
secret: env.BETTER_AUTH_SECRET,
14-
socialProviders: {
15-
github: {
16-
clientId: env.GITHUB_CLIENT_ID,
17-
clientSecret: env.GITHUB_CLIENT_SECRET,
18-
scope: ["read:user", "user:email", "read:org"],
19-
disableDefaultScope: true,
20-
},
7+
type AuthType = ReturnType<typeof betterAuth>;
8+
9+
let _auth: AuthType | null = null;
10+
11+
function getAuth(): AuthType {
12+
if (!_auth) {
13+
_auth = betterAuth({
14+
database: drizzleAdapter(db, {
15+
provider: "pg",
16+
schema,
17+
}),
18+
baseURL: env.BETTER_AUTH_URL,
19+
secret: env.BETTER_AUTH_SECRET,
20+
socialProviders: {
21+
github: {
22+
clientId: env.GITHUB_CLIENT_ID,
23+
clientSecret: env.GITHUB_CLIENT_SECRET,
24+
scope: ["read:user", "user:email", "read:org"],
25+
disableDefaultScope: true,
26+
},
27+
},
28+
});
29+
}
30+
return _auth;
31+
}
32+
33+
// Lazy-initialized auth instance via Proxy
34+
export const auth: AuthType = new Proxy({} as AuthType, {
35+
get(_, prop) {
36+
const instance = getAuth();
37+
const value = instance[prop as keyof AuthType];
38+
if (typeof value === "function") {
39+
return (value as (...args: unknown[]) => unknown).bind(instance);
40+
}
41+
return value;
2142
},
2243
});

src/lib/server/drivers/db.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
12
import { drizzle } from "drizzle-orm/postgres-js";
23
import postgres from "postgres";
34
import { env } from "$lib/env/env.server";
45
import * as schema from "$lib/shared/models/schema";
56

6-
const client = postgres(env.DATABASE_URL);
7+
type DbType = PostgresJsDatabase<typeof schema>;
78

8-
export const db = drizzle(client, { schema });
9+
let _db: DbType | null = null;
10+
11+
function getDb(): DbType {
12+
if (!_db) {
13+
const client = postgres(env.DATABASE_URL);
14+
_db = drizzle(client, { schema });
15+
}
16+
return _db;
17+
}
18+
19+
// Lazy-initialized db instance via Proxy
20+
export const db: DbType = new Proxy({} as DbType, {
21+
get(_, prop) {
22+
const instance = getDb();
23+
const value = instance[prop as keyof DbType];
24+
if (typeof value === "function") {
25+
return value.bind(instance);
26+
}
27+
return value;
28+
},
29+
});

src/lib/server/drivers/s3.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,60 @@
11
import { Client } from "minio";
22
import { env } from "$lib/env/env.server";
33

4-
const url = new URL(env.S3_ENDPOINT);
4+
let _s3: Client | null = null;
55

6-
export const s3 = new Client({
7-
endPoint: url.hostname,
8-
port: url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80,
9-
useSSL: url.protocol === "https:",
10-
accessKey: env.S3_ACCESS_KEY,
11-
secretKey: env.S3_SECRET_KEY,
6+
function getS3(): Client {
7+
if (!_s3) {
8+
const url = new URL(env.S3_ENDPOINT);
9+
_s3 = new Client({
10+
endPoint: url.hostname,
11+
port: url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80,
12+
useSSL: url.protocol === "https:",
13+
accessKey: env.S3_ACCESS_KEY,
14+
secretKey: env.S3_SECRET_KEY,
15+
});
16+
}
17+
return _s3;
18+
}
19+
20+
// Lazy-initialized s3 client via Proxy
21+
export const s3: Client = new Proxy({} as Client, {
22+
get(_, prop) {
23+
const instance = getS3();
24+
const value = instance[prop as keyof Client];
25+
if (typeof value === "function") {
26+
return (value as (...args: unknown[]) => unknown).bind(instance);
27+
}
28+
return value;
29+
},
1230
});
1331

14-
export const bucket = env.S3_BUCKET;
32+
export function getBucket(): string {
33+
return env.S3_BUCKET;
34+
}
1535

1636
// Ensure bucket exists and is public (for dev)
17-
const publicPolicy = {
18-
Version: "2012-10-17",
19-
Statement: [
20-
{
21-
Effect: "Allow",
22-
Principal: { AWS: ["*"] },
23-
Action: ["s3:GetObject"],
24-
Resource: [`arn:aws:s3:::${bucket}/*`],
25-
},
26-
],
27-
};
28-
2937
let initialized = false;
3038
export async function ensureBucket() {
3139
if (initialized) return;
3240
initialized = true;
3341

34-
const exists = await s3.bucketExists(bucket);
42+
const bucketName = env.S3_BUCKET;
43+
const publicPolicy = {
44+
Version: "2012-10-17",
45+
Statement: [
46+
{
47+
Effect: "Allow",
48+
Principal: { AWS: ["*"] },
49+
Action: ["s3:GetObject"],
50+
Resource: [`arn:aws:s3:::${bucketName}/*`],
51+
},
52+
],
53+
};
54+
55+
const exists = await s3.bucketExists(bucketName);
3556
if (!exists) {
36-
await s3.makeBucket(bucket);
57+
await s3.makeBucket(bucketName);
3758
}
38-
await s3.setBucketPolicy(bucket, JSON.stringify(publicPolicy));
59+
await s3.setBucketPolicy(bucketName, JSON.stringify(publicPolicy));
3960
}

0 commit comments

Comments
 (0)