Mike Cann's avatar
Mike Cann
3 minutes ago

Type-Safe Environment Variables in Convex

Have you ever had this happen to you? You (or your coding agent) are working away on some Convex code, everything is going great with the schemas, queries and mutations, then someone reaches for process.env.OPEN_AI_KEY instead of OPENAI_API_KEY. TypeScript says nothing, and you only find out at runtime.

That's because process.env is stringly typed. There's no way for the type system (or an agent) to know which variables exist or what shape their values should take.

Well NO MORE, as a new feature silently dropped in Convex 1.39.0 a couple of weeks ago that has largely flown under the radar but I think is really awesome. You can now declare your deployment's environment variables in code, with validators, and get a fully typed env object to use in your backend functions:

1process.env.OPEN_AI_KEY; // before
2env.OPENAI_API_KEY;      // after
3

Let's take a look.

Declaring environment variables

So first make sure you are running Convex 1.39.0 or later. Then pop open your convex/convex.config.ts file and as part of your defineApp call you can now declare what environment variables your app expects:

1// convex/convex.config.ts
2import { defineApp } from "convex/server";
3import { v } from "convex/values";
4
5const app = defineApp({
6  env: {
7    OPENAI_API_KEY: v.string(),
8    LOG_LEVEL: v.optional(
9      v.union(v.literal("debug"), v.literal("info"), v.literal("error")),
10    ),
11  },
12});
13
14export default app;
15

You can use validators from convex/values to declare what sort of shape you expect your vars to take, the same ones you already know from schemas and function arguments. One thing to note though: because env var values are always strings, only a subset of validators is supported here:

  • v.string(): any string value
  • v.literal("value"): an exact string value
  • v.union(v.literal("a"), v.literal("b")): one of several exact values
  • v.optional(...): wraps any of the above to mark a variable as optional

So no v.number() I'm afraid, at least for now.

Now after you run npx convex dev, Convex is going to generate a typed env object that you can import inside your backend functions:

1import { env } from "./_generated/server";
2
3const openAiKey = env.OPENAI_API_KEY;
4

Super cool! Autocomplete works, typos are compile errors, and the value's type matches its validator.

Setting the variables

Now you obviously still need to set these environment variables on your Convex deployment, declaring them just tells Convex what to expect. You can set them in the dashboard:

Convex dashboard environment variable settingsConvex dashboard environment variable settings

Or via the CLI:

1npx convex env set OPENAI_API_KEY
2

If you don't set a required variable, Convex is going to yell at you when you push:

1Error: Unable to finish push to https://secret-buzzard-634.convex.cloud
2Error fetching POST https://secret-buzzard-634.convex.cloud/api/deploy2/finish_push 400 Bad Request: MissingEnvironmentVariables: Hit an error while pushing:
3Required environment variables are not set: OPENAI_API_KEY. Set them in the Convex dashboard or CLI before pushing.
4

And it's also going to yell at you if a value doesn't match its validator:

1Error fetching POST https://secret-buzzard-634.convex.cloud/api/deploy2/finish_push 400 Bad Request: InvalidEnvironmentVariable: Hit an error while pushing:
2Environment variable LOG_LEVEL does not match its declared validator: Value does not match validator.
3
4Value: "foo"
5Validator: v.union(v.literal("debug"), v.literal("info"), v.literal("error"))
6

Also really good.

By the way, notice that as you change an env var the dev process immediately picks it up and rebuilds, such a nice little quality of life thing:

109:49:27 Convex functions ready! (2.92s)
2

And this isn't just a push-time thing either. If you were to later change or remove a variable from the dashboard or CLI in a way that breaks the declaration, Convex isn't going to let that through, which protects us. Noice!

Why not Zod?

A natural question is why you can't use Zod or other validation libraries for the env var validation?

Environment variable validation happens in the Rust layer of Convex, where arbitrary JavaScript can't run, so right now we are limiting it to Convex validators.

Typed environment variables for components

Oh and components got a big upgrade here too. Component functions are isolated from the app's environment variables, so a component can declare the env vars it needs like this:

1// my-component/convex.config.ts
2import { defineComponent } from "convex/server";
3import { v } from "convex/values";
4
5const component = defineComponent("myComponent", {
6  env: {
7    API_KEY: v.string(),
8    MODE: v.union(v.literal("test"), v.literal("live")),
9  },
10});
11
12export default component;
13

And the parent app can wire those up when it uses the component, either by reference to one of its own variables or with a literal value:

1// convex/convex.config.ts
2import { defineApp } from "convex/server";
3import { v } from "convex/values";
4import myComponent from "@example/my-component/convex.config";
5
6const app = defineApp({
7  env: {
8    OPENAI_API_KEY: v.string(),
9    MY_COMPONENT_API_KEY: v.string(),
10  },
11});
12
13app.use(myComponent, {
14  env: {
15    // Pass by reference: stays in sync with the app's variable,
16    // even when it changes between deploys
17    API_KEY: app.env.MY_COMPONENT_API_KEY,
18    // Or pass a literal value
19    MODE: "live",
20  },
21});
22
23export default app;
24

If a component declares required env vars then the app that installs it has to provide them, or the push fails. So no longer do you have to wonder whether you have set all the environment variables a component needs, only to have it crash on you at runtime!

Conclusion

So just to recap: you can now tell Convex that you expect some environment variables to be set for your deployment and that they take a particular type, and Convex enforces it in your editor, at push time, and on any later changes to those variables.

I love this because it feels very Convex-y doesn't it? Everything in Convex is just code, like your schema and your generated API, so it feels natural that the missing piece of state, the environment variables, gets the same treatment.

This is also obviously great for agents, for the same reason agents love Convex schemas and generated APIs: they get immediate context on what they can expect to be there at runtime and fast feedback when they get something wrong.

By the way, if you do still want to use process.env then go ahead, it ain't going anywhere, this new typed env stuff isn't a breaking change.

Until next time, cheerio!

Build in minutes, scale forever.

Convex is the backend platform with everything you need to build your full-stack AI project. Cloud functions, a database, file storage, scheduling, workflow, vector search, and realtime updates fit together seamlessly.

Get started