feat: integrate @t3-oss/env for environment variable management across the application

This commit is contained in:
tranminhquang
2025-07-15 13:29:16 +07:00
parent 55978c30fa
commit 9d5fb821c7
16 changed files with 118 additions and 37 deletions
+4 -7
View File
@@ -1,23 +1,20 @@
import type { Config } from "drizzle-kit"; import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { env } from "@/env";
// Load the right env file based on environment // Load the right env file based on environment
if (process.env.NODE_ENV === "production") { if (env.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" }); dotenv.config({ path: ".env.production" });
} else { } else {
dotenv.config({ path: ".env.local" }); dotenv.config({ path: ".env.local" });
} }
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
}
export default { export default {
schema: "../../packages/db/src/schema.ts", schema: "../../packages/db/src/schema.ts",
dialect: "postgresql", dialect: "postgresql",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL, url: env.DATABASE_URL,
}, },
out: "./migrations", out: "./migrations",
strict: process.env.NODE_ENV === "production", strict: env.NODE_ENV === "production",
} satisfies Config; } satisfies Config;
+2
View File
@@ -21,6 +21,8 @@
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@opencut/auth": "workspace:*", "@opencut/auth": "workspace:*",
"@opencut/db": "workspace:*", "@opencut/db": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@t3-oss/env-nextjs": "^0.13.8",
"@upstash/ratelimit": "^2.0.5", "@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.35.0", "@upstash/redis": "^1.35.0",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.4.1",
@@ -7,9 +7,10 @@ import { usePlaybackStore } from "@/stores/playback-store";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState } from "react"; import { useState } from "react";
import type { TimelineElement } from "@/types/timeline"; import type { TimelineElement } from "@/types/timeline";
import { env } from "@/env";
// Only show in development // Only show in development
const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development"; const SHOW_DEBUG_INFO = env.NODE_ENV === "development";
interface ActiveElement { interface ActiveElement {
element: TimelineElement; element: TimelineElement;
+2 -1
View File
@@ -8,6 +8,7 @@ import { useSession } from "@opencut/auth/client";
import { getStars } from "@/lib/fetch-github-stars"; import { getStars } from "@/lib/fetch-github-stars";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { env } from "@/env";
export function Header() { export function Header() {
const { data: session } = useSession(); const { data: session } = useSession();
@@ -40,7 +41,7 @@ export function Header() {
Contributors Contributors
</Button> </Button>
</Link> </Link>
{process.env.NODE_ENV === "development" ? ( {env.NODE_ENV === "development" ? (
<Link href="/projects"> <Link href="/projects">
<Button size="sm" className="text-sm ml-4"> <Button size="sm" className="text-sm ml-4">
Projects Projects
+27
View File
@@ -0,0 +1,27 @@
import { vercel } from "@t3-oss/env-core/presets-zod";
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { keys as auth } from "@opencut/auth/keys";
import { keys as db } from "@opencut/db/keys";
export const env = createEnv({
extends: [vercel(), auth(), db()],
server: {
ANALYZE: z.string().optional(),
// Added by Vercel
NEXT_RUNTIME: z.enum(["nodejs", "edge"]).optional(),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
UPSTASH_REDIS_REST_URL: z.string().url(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
},
client: {},
runtimeEnv: {
ANALYZE: process.env.ANALYZE,
NEXT_RUNTIME: process.env.NEXT_RUNTIME,
NODE_ENV: process.env.NODE_ENV,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
},
});
+3 -2
View File
@@ -1,10 +1,11 @@
// lib/rate-limit.ts // lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit"; import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis"; import { Redis } from "@upstash/redis";
import { env } from "@/env";
const redis = new Redis({ const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!, url: env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN!, token: env.UPSTASH_REDIS_REST_TOKEN,
}); });
export const waitlistRateLimit = new Ratelimit({ export const waitlistRateLimit = new Ratelimit({
+2 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { env } from "./env";
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
// Handle fuckcapcut.com domain redirect // Handle fuckcapcut.com domain redirect
@@ -9,7 +10,7 @@ export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname; const path = request.nextUrl.pathname;
if (path === "/editor" && process.env.NODE_ENV === "production") { if (path === "/editor" && env.NODE_ENV === "production") {
const homeUrl = new URL("/", request.url); const homeUrl = new URL("/", request.url);
homeUrl.searchParams.set("redirect", request.url); homeUrl.searchParams.set("redirect", request.url);
return NextResponse.redirect(homeUrl); return NextResponse.redirect(homeUrl);
+6 -3
View File
@@ -7,13 +7,16 @@
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./client": "./src/client.ts", "./client": "./src/client.ts",
"./server": "./src/server.ts" "./server": "./src/server.ts",
"./keys": "./src/keys.ts"
}, },
"dependencies": { "dependencies": {
"@opencut/db": "workspace:*",
"@t3-oss/env-nextjs": "^0.13.8",
"better-auth": "^1.1.1", "better-auth": "^1.1.1",
"@opencut/db": "workspace:*" "zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.2" "@types/node": "^22.10.2"
} }
} }
+5 -2
View File
@@ -1,5 +1,8 @@
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { keys } from "./keys";
const { NEXT_PUBLIC_BETTER_AUTH_URL } = keys();
export const { signIn, signUp, useSession } = createAuthClient({ export const { signIn, signUp, useSession } = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL!, baseURL: NEXT_PUBLIC_BETTER_AUTH_URL,
}); });
+4 -1
View File
@@ -2,4 +2,7 @@
export * from "./server"; export * from "./server";
// Re-export client auth // Re-export client auth
export * from "./client"; export * from "./client";
// Re-export keys
export * from "./keys";
+20
View File
@@ -0,0 +1,20 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const keys = () =>
createEnv({
server: {
BETTER_AUTH_SECRET: z.string(),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
},
client: {
NEXT_PUBLIC_BETTER_AUTH_URL: z.string().url(),
},
runtimeEnv: {
NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
},
});
+7 -4
View File
@@ -1,13 +1,16 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@opencut/db"; import { db } from "@opencut/db";
import { keys } from "./keys";
const { NEXT_PUBLIC_BETTER_AUTH_URL, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } = keys()
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
usePlural: true, usePlural: true,
}), }),
secret: process.env.BETTER_AUTH_SECRET, secret: BETTER_AUTH_SECRET,
user: { user: {
deleteUser: { deleteUser: {
enabled: true, enabled: true,
@@ -18,11 +21,11 @@ export const auth = betterAuth({
}, },
socialProviders: { socialProviders: {
google: { google: {
clientId: process.env.GOOGLE_CLIENT_ID as string, clientId: GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, clientSecret: GOOGLE_CLIENT_SECRET,
}, },
}, },
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL, baseURL: NEXT_PUBLIC_BETTER_AUTH_URL,
appName: "OpenCut", appName: "OpenCut",
trustedOrigins: ["http://localhost:3000"], trustedOrigins: ["http://localhost:3000"],
}); });
+7 -8
View File
@@ -1,17 +1,16 @@
import type { Config } from "drizzle-kit"; import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { keys } from "./src/keys";
const { NODE_ENV, DATABASE_URL } = keys();
// Load the right env file based on environment // Load the right env file based on environment
if (process.env.NODE_ENV === "production") { if (NODE_ENV === "production") {
dotenv.config({ path: ".env.production" }); dotenv.config({ path: ".env.production" });
} else { } else {
dotenv.config({ path: ".env.local" }); dotenv.config({ path: ".env.local" });
} }
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
}
export default { export default {
schema: "./src/schema.ts", schema: "./src/schema.ts",
dialect: "postgresql", dialect: "postgresql",
@@ -19,8 +18,8 @@ export default {
table: "drizzle_migrations", table: "drizzle_migrations",
}, },
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL, url: DATABASE_URL,
}, },
out: "./migrations", out: "./migrations",
strict: process.env.NODE_ENV === "production", strict: NODE_ENV === "production",
} satisfies Config; } satisfies Config;
+5 -2
View File
@@ -7,7 +7,8 @@
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./schema": "./src/schema.ts", "./schema": "./src/schema.ts",
"./drizzle.config": "./drizzle.config.ts" "./drizzle.config": "./drizzle.config.ts",
"./keys": "./src/keys.ts"
}, },
"scripts": { "scripts": {
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
@@ -16,8 +17,10 @@
"db:studio": "drizzle-kit studio" "db:studio": "drizzle-kit studio"
}, },
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.44.2",
"postgres": "^3.4.5" "postgres": "^3.4.5",
"zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
+3 -5
View File
@@ -1,17 +1,15 @@
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import * as schema from "./schema"; import * as schema from "./schema";
import { keys } from "./keys";
const { DATABASE_URL } = keys();
// Create a lazy database instance that only initializes when accessed // Create a lazy database instance that only initializes when accessed
let _db: ReturnType<typeof drizzle> | null = null; let _db: ReturnType<typeof drizzle> | null = null;
function getDb() { function getDb() {
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
}
if (!_db) { if (!_db) {
const client = postgres(process.env.DATABASE_URL); const client = postgres(DATABASE_URL);
_db = drizzle(client, { schema }); _db = drizzle(client, { schema });
} }
+19
View File
@@ -0,0 +1,19 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const keys = () =>
createEnv({
server: {
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
DATABASE_URL: z
.string()
.startsWith("postgres://")
.or(z.string().startsWith("postgresql://")),
},
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
DATABASE_URL: process.env.DATABASE_URL,
},
});