T3 Env + Standard Schema

Using T3 Env with @cleverbrush/schema for type-safe, validated environment variables via the Standard Schema v1 interface.

How it works

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.ts

Drop 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.ts

When 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;

Usage

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'} />;
}

Live Validation Demo

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

✓ valid
✓ valid
✓ valid
✓ valid

Client variables

✓ valid
✓ valid

Why this works: Standard Schema interop

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'