Using T3 Env with @cleverbrush/schema for type-safe, validated environment variables via the Standard Schema v1 interface.
T3 Env accepts any Standard Schema v1 compliant validator in its server and client objects. Every @cleverbrush/schema builder exposes a ["~standard"] property, so schemas can be passed directly — no adapter, no wrapper.
T3 Env calls schema['~standard'].validate(value) for each environment variable at startup. If validation fails, the app crashes with a clear error listing which variables are invalid — before a single request is served.
app/env.tsDrop this file in your Next.js app. Every @cleverbrush/schema type goes straight into the server / client map:
import { createEnv } from '@t3-oss/env-nextjs';
import { boolean, number, string } from '@cleverbrush/schema';
export const env = createEnv({
server: {
DATABASE_URL: string()
.required('DATABASE_URL is required')
.matches(
/^(postgres|postgresql|mysql|sqlite):\/\/.+/,
'DATABASE_URL must be a valid database connection string'
),
API_SECRET: string()
.required('API_SECRET is required')
.minLength(32, 'API_SECRET must be at least 32 characters'),
PORT: number().min(1, 'PORT must be ≥ 1').max(65535, 'PORT must be ≤ 65535'),
LOG_LEVEL: string()
.required('LOG_LEVEL is required')
.matches(
/^(debug|info|warn|error)$/,
'LOG_LEVEL must be one of: debug, info, warn, error'
),
},
client: {
NEXT_PUBLIC_API_URL: string()
.required('NEXT_PUBLIC_API_URL is required')
.matches(/^https?:\/\/.+/, 'Must be a valid http/https URL'),
NEXT_PUBLIC_ENABLE_ANALYTICS: boolean(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET: process.env.API_SECRET,
PORT: process.env.PORT ? Number(process.env.PORT) : undefined,
LOG_LEVEL: process.env.LOG_LEVEL,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_ENABLE_ANALYTICS:
process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
},
});next.config.tsWhen using the standalone output mode, add the T3 Env packages to transpilePackages so Next.js bundles them correctly:
import './app/env'; // validates at build time
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
transpilePackages: ['@t3-oss/env-nextjs', '@t3-oss/env-core'],
};
export default nextConfig;Import env anywhere in your app. TypeScript infers the exact type of each variable from the schema:
// Server component or API route
import { env } from '~/app/env';
export async function GET() {
const db = createPool(env.DATABASE_URL); // string — validated URL
const port = env.PORT; // number | undefined
return new Response('ok');
}
// Client component
import { env } from '~/app/env';
export function AnalyticsBanner() {
if (!env.NEXT_PUBLIC_ENABLE_ANALYTICS) return null;
return <script src={env.NEXT_PUBLIC_API_URL + '/analytics.js'} />;
}Simulates exactly what T3 Env does at startup: passes each value through schema['~standard'].validate(). Try entering invalid values to see the error messages.
Edit any value to see live validation powered by schema['~standard'].validate() — the same call T3 Env makes at startup.
Server variables
Client variables
T3 Env checks for the presence of a ['~standard'] property on each validator and, if present, delegates validation to validator['~standard'].validate(value). This is the Standard Schema v1 contract. Since every @cleverbrush/schema builder exposes this property, the integration is zero-config:
import { string } from '@cleverbrush/schema';
const schema = string().required('required').minLength(8, 'too short');
// T3 Env calls this internally:
const result = schema['~standard'].validate('hi');
// result => { issues: [{ message: 'too short' }] }
const ok = schema['~standard'].validate('long enough value');
// ok => { value: 'long enough value' }
console.log(schema['~standard'].version); // => 1
console.log(schema['~standard'].vendor); // => '@cleverbrush/schema'