diff --git a/.changeset/fifty-flowers-mix.md b/.changeset/fifty-flowers-mix.md
new file mode 100644
index 00000000..b94eac28
--- /dev/null
+++ b/.changeset/fifty-flowers-mix.md
@@ -0,0 +1,5 @@
+---
+"@t3-oss/env-core": minor
+---
+
+feat: add option `emptyStringAsUndefined`
diff --git a/docs/src/app/docs/core/page.mdx b/docs/src/app/docs/core/page.mdx
index c64c6ff4..46b7e04c 100644
--- a/docs/src/app/docs/core/page.mdx
+++ b/docs/src/app/docs/core/page.mdx
@@ -30,7 +30,7 @@ Then, you can create your schema like so:
-The file below is named `env.ts`, but you can name it whatever you want. Some frameworks even generate a `env.d.ts` file that will collide with `env.ts` which means you'll have to name it something else.
+The file below is named `env.ts`, but you can name it whatever you want. Some frameworks even generate a `env.d.ts` file that will collide with `env.ts`, which means you will have to name it something else.
@@ -39,26 +39,46 @@ import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export const env = createEnv({
- /**
- * Specify what prefix the client-side variables must have.
- * This is enforced both on type-level and at runtime.
- */
- clientPrefix: "PUBLIC_",
server: {
DATABASE_URL: z.string().url(),
OPEN_AI_API_KEY: z.string().min(1),
},
+
+ /**
+ * The prefix that client-side variables must have. This is enforced both at
+ * a type-level and at runtime.
+ */
+ clientPrefix: "PUBLIC_",
+
client: {
PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
},
+
/**
- * What object holds the environment variables at runtime.
- * Often `process.env` or `import.meta.env`
+ * What object holds the environment variables at runtime. This is usually
+ * `process.env` or `import.meta.env`.
*/
runtimeEnv: process.env,
+
+ /**
+ * By default, this library will feed the environment variables directly to
+ * the Zod validator.
+ *
+ * This means that if you have an empty string for a value that is supposed
+ * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag
+ * it as a type mismatch violation. Additionally, if you have an empty string
+ * for a value that is supposed to be a string with a default value (e.g.
+ * `DOMAIN=` in an ".env" file), the default value will never be applied.
+ *
+ * In order to solve these issues, we recommend that all new projects
+ * explicitly specify this option as true.
+ */
+ emptyStringAsUndefined: true,
});
```
+Remove the `clientPrefix` and `client` properties if you only want the environment variables to exist on the server.
+
While defining both the client and server schemas in a single file provides the best developer experience,
diff --git a/packages/core/index.ts b/packages/core/index.ts
index c486e525..f54c3ecd 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -46,10 +46,12 @@ export interface BaseOptions> {
export interface LooseOptions>
extends BaseOptions {
runtimeEnvStrict?: never;
+
/**
- * Runtime Environment variables to use for validation - `process.env`, `import.meta.env` or similar.
- * Unlike `runtimeEnvStrict`, this doesn't enforce that all environment variables are set.
+ * What object holds the environment variables at runtime. This is usually
+ * `process.env` or `import.meta.env`.
*/
+ // Unlike `runtimeEnvStrict`, this doesn't enforce that all environment variables are set.
runtimeEnv: Record;
}
@@ -87,13 +89,14 @@ export interface ClientOptions<
TClient extends Record
> {
/**
- * Client-side environment variables are exposed to the client by default. Set what prefix they have
+ * The prefix that client-side variables must have. This is enforced both at
+ * a type-level and at runtime.
*/
clientPrefix: TPrefix;
/**
* Specify your client-side environment variables schema here. This way you can ensure the app isn't
- * built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`.
+ * built with invalid env vars.
*/
client: Partial<{
[TKey in keyof TClient]: TKey extends `${TPrefix}${string}`
@@ -121,6 +124,21 @@ export interface ServerOptions<
: never} should not prefixed with ${TPrefix}.`>
: TServer[TKey];
}>;
+
+ /**
+ * By default, this library will feed the environment variables directly to
+ * the Zod validator.
+ *
+ * This means that if you have an empty string for a value that is supposed
+ * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag
+ * it as a type mismatch violation. Additionally, if you have an empty string
+ * for a value that is supposed to be a string with a default value (e.g.
+ * `DOMAIN=` in an ".env" file), the default value will never be applied.
+ *
+ * In order to solve these issues, we recommend that all new projects
+ * explicitly specify this option as true.
+ */
+ emptyStringAsUndefined?: boolean;
}
export type ServerClientOptions<
@@ -158,6 +176,15 @@ export function createEnv<
> {
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
+ const emptyStringAsUndefined = opts.emptyStringAsUndefined ?? false;
+ if (emptyStringAsUndefined) {
+ for (const [key, value] of Object.entries(runtimeEnv)) {
+ if (value === "") {
+ delete runtimeEnv[key];
+ }
+ }
+ }
+
const skip = !!opts.skipValidation;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
if (skip) return runtimeEnv as any;