feat: type safe env (#161)

This commit is contained in:
Rahul Mishra
2025-02-13 21:52:48 +05:30
committed by Jacob Samorowski
parent 1a46ce98a9
commit abffebccc1
13 changed files with 123 additions and 31 deletions

View File

@@ -9,13 +9,14 @@ BETTER_AUTH_URL=http://localhost:3000
# Change to your project's client ID and secret, these work with localhost:3000 and localhost:3001
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:3000/api/v1/mail/auth/google/callback
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Upstash Redis Instance
REDIS_URL=""
REDIS_TOKEN=""
# Upstash/Local Redis Instance
REDIS_URL="http://localhost:8079"
REDIS_TOKEN="upstash-local-token"
# Resend API Key
RESEND_API_KEY=

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { earlyAccess } from "@/db/schema";
import { randomUUID } from "node:crypto";
import { env } from "@/lib/env";
import { db } from "@/db";
type PostgresError = {
@@ -88,7 +89,7 @@ export async function POST(req: NextRequest) {
});
// Return more detailed error in development
if (process.env.NODE_ENV === "development") {
if (env.NODE_ENV === "development") {
return NextResponse.json(
{
error: "Internal server error",

View File

@@ -2,6 +2,7 @@
import { ParsedMessage } from "@/types";
import { google } from "googleapis";
import { env } from "@/lib/env";
import * as he from "he";
interface MailManager {
@@ -53,9 +54,9 @@ const findHtmlBody = (parts: any[]): string => {
const googleDriver = async (config: IConfig): Promise<MailManager> => {
const auth = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID as string,
process.env.GOOGLE_CLIENT_SECRET as string,
process.env.GOOGLE_REDIRECT_URI as string,
env.GOOGLE_CLIENT_ID as string,
env.GOOGLE_CLIENT_SECRET as string,
env.GOOGLE_REDIRECT_URI as string,
);
const getScope = () =>

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { createDriver } from "@/app/api/driver";
import { connection } from "@/db/schema";
import { env } from "@/lib/env";
import { db } from "@/db";
export async function GET(
@@ -12,9 +13,7 @@ export async function GET(
const state = searchParams.get("state");
if (!code || !state) {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/settings/email?error=missing_params`,
);
return NextResponse.redirect(`${env.NEXT_PUBLIC_APP_URL}/settings/email?error=missing_params`);
}
const { providerId } = await params;

View File

@@ -2,6 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
import { env } from "@/lib/env";
/**
* Cache the database connection in development. This avoids creating a new connection on every HMR
@@ -11,7 +12,7 @@ const globalForDb = globalThis as unknown as {
conn: postgres.Sql | undefined;
};
const conn = globalForDb.conn ?? postgres(process.env.DATABASE_URL!);
if (process.env.NODE_ENV !== "production") globalForDb.conn = conn;
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL!);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });

View File

@@ -2,16 +2,38 @@ services:
db:
container_name: mail0-db
image: postgres:17
restart: always
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mail0
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
valkey:
container_name: mail0-redis
image: docker.io/bitnami/valkey:8.0
environment:
- ALLOW_EMPTY_PASSWORD=yes
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
ports:
- 6379:6379
volumes:
- valkey-data:/bitnami/valkey/data
upstash-proxy:
container_name: mail0-upstash-proxy
image: hiett/serverless-redis-http:latest
environment:
SRH_MODE: env
SRH_TOKEN: upstash-local-token
SRH_CONNECTION_STRING: "redis://valkey:6379"
ports:
- 8079:80
volumes:
valkey-data:
postgres-data:

View File

@@ -1,10 +1,11 @@
import { type Config } from "drizzle-kit";
import { env } from "./lib/env";
export default {
schema: "./db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
url: env.DATABASE_URL!,
},
out: "./db/migrations",
tablesFilter: ["mail0_*"],

View File

@@ -1,10 +1,4 @@
import { Redis } from "@upstash/redis";
import { env } from "@/lib/env";
const url = process.env.REDIS_URL;
const token = process.env.REDIS_TOKEN;
if (!url || !token) {
throw new Error("Missing Redis URL or token");
}
export const redis = new Redis({ url, token });
export const redis = new Redis({ url: env.REDIS_URL, token: env.REDIS_TOKEN });

View File

@@ -1,11 +1,10 @@
import { customSessionClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import type { auth } from "@/lib/auth"; // Import the auth instance as a type
const BASE_URL = process.env.BASE_URL as string;
import { env } from "./env";
export const authClient = createAuthClient({
baseURL: BASE_URL, // the base url of your auth server
baseURL: env.NEXT_PUBLIC_APP_URL,
plugins: [customSessionClient<typeof auth>()],
});

View File

@@ -5,12 +5,13 @@ import { connection, user as _user } from "@/db/schema";
import { customSession } from "better-auth/plugins";
import { eq } from "drizzle-orm";
import { Resend } from "resend";
import { env } from "./env";
import { db } from "@/db";
// If there is no resend key, it might be a local dev environment
// In that case, we don't want to send emails and just log them
const resend = process.env.RESEND_API_KEY
? new Resend(process.env.RESEND_API_KEY)
const resend = env.RESEND_API_KEY
? new Resend(env.RESEND_API_KEY)
: { emails: { send: async (...args: any[]) => console.log(args) } };
const options = {
@@ -27,8 +28,8 @@ const options = {
prompt: "consent",
accessType: "offline",
scope: ["https://mail.google.com/"],
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
emailAndPassword: {
@@ -52,7 +53,7 @@ const options = {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, token }) => {
const verificationUrl = `${process.env.BASE_URL}/api/auth/verify-email?token=${token}&callbackURL=/connect-emails`;
const verificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/auth/verify-email?token=${token}&callbackURL=/connect-emails`;
await resend.emails.send({
from: "Mail0 <onboarding@mail0.io>",

28
lib/env.ts Normal file
View File

@@ -0,0 +1,28 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
DATABASE_URL: z.string().min(1),
BETTER_AUTH_SECRET: z.string().min(1),
BETTER_AUTH_URL: z.string().min(1).url(),
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
GOOGLE_REDIRECT_URI: z.string().min(1).url(),
REDIS_URL: z.string().min(1).url(),
REDIS_TOKEN: z.string().min(1),
RESEND_API_KEY: z.string().min(1).optional(),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1).url(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
...process.env,
},
});

View File

@@ -38,6 +38,7 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.1.2",
"@t3-oss/env-nextjs": "^0.12.0",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.4",
"axios": "^1.7.9",

43
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
'@radix-ui/react-visually-hidden':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@t3-oss/env-nextjs':
specifier: ^0.12.0
version: 0.12.0(typescript@5.7.3)(zod@3.24.1)
'@upstash/ratelimit':
specifier: ^2.0.5
version: 2.0.5(@upstash/redis@1.34.4)
@@ -1491,6 +1494,34 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@t3-oss/env-core@0.12.0':
resolution: {integrity: sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==}
peerDependencies:
typescript: '>=5.0.0'
valibot: ^1.0.0-beta.7 || ^1.0.0
zod: ^3.24.0
peerDependenciesMeta:
typescript:
optional: true
valibot:
optional: true
zod:
optional: true
'@t3-oss/env-nextjs@0.12.0':
resolution: {integrity: sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw==}
peerDependencies:
typescript: '>=5.0.0'
valibot: ^1.0.0-beta.7 || ^1.0.0
zod: ^3.24.0
peerDependenciesMeta:
typescript:
optional: true
valibot:
optional: true
zod:
optional: true
'@tailwindcss/typography@0.5.16':
resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
peerDependencies:
@@ -5018,6 +5049,18 @@ snapshots:
dependencies:
tslib: 2.8.1
'@t3-oss/env-core@0.12.0(typescript@5.7.3)(zod@3.24.1)':
optionalDependencies:
typescript: 5.7.3
zod: 3.24.1
'@t3-oss/env-nextjs@0.12.0(typescript@5.7.3)(zod@3.24.1)':
dependencies:
'@t3-oss/env-core': 0.12.0(typescript@5.7.3)(zod@3.24.1)
optionalDependencies:
typescript: 5.7.3
zod: 3.24.1
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)':
dependencies:
lodash.castarray: 4.4.0