This is the full developer documentation for DMNO
# Type-safe and secure Astro configuration with DMNO
 *Astro CTO Matthew Phillips presenting Astro’s new `astro:env` feature at Astro Together in Montreal.*
Configuration in Astro has improved drastically in recent weeks. We launched DMNO, including our [Astro integration](/docs/integrations/astro/), and shortly after Astro launched their official (experimental) `astro:env` feature in version [`4.10`](https://astro.build/blog/astro-4100/). We were very excited to see this on the main stage at [Astro Together in Montreal](https://astro.build/blog/astro-together-montreal/) (see above ^^), and to talk with the core team about how we can work together to make configuration in Astro even better. There’s already some great [cross-pollination](https://github.com/withastro/roadmap/discussions/956) happening, and we’re excited to see where it goes.
We’re really glad to see Astro taking configuration seriously, and we’re excited to see how the community uses these new tools to build even better sites. That said, we do think DMNO has some unique advantages that make it a great choice for many Astro projects, especially if you want to share config among other services, or collaborate securely with your teammates.
Here’s a quick overview of what DMNO can do for you:
### Key Features
* **True type-safety** Your config values are coerced, validated, and accessible with complete type-safety. Fail fast and early rather than bringing down production. 
* **Built-in docs for your config** We generate very rich types from your config schema, giving you built-in docs (Intellisense) for all of your config. 
* **Store secrets securely:** Use plugins to store and sync your sensitive config items in an encrypted file within your repo or remotely in 1Password. More secure backends coming soon! No more insecurely sharing secrets over slack! 
* **Leak detection:** DMNO ensures that only non-sensitive config is accessible on the client side, and ensures sensitive config is never leaked by injecting an Astro middleware that scans rendered server responses before sending them over the wire. 
* **Log redaction:** DMNO redacts sensitive data from all global `console` method output, ensuring that your secrets are never exposed or persisted in your logs. 
* **HTTP interception:** DMNO intercepts HTTP requests to prevent sensitive data from being sent to the wrong third-party services. e.g., Only send your Stripe key to `api.stripe.com`. 
* **Share config items in your monorepo:** Unified config system for all of your services, with the ability to easy reuse items from the root or other services. Even outside of monorepos, using the same config system for your entire stack simplifies things.
* **Static and dynamic config:** Fine control over which items are static (injected at build time) vs dynamic (loaded at boot), with additional safe-guards around using dynamic config during static pre-rendering. We even support dynamic public config loaded at runtime.
**Plus it’s super easy to get started:**
* **Automatic initialization:** Using `dmno init`, DMNO detects your Astro setup, automatically installs necessary packages, and updates your Astro config file.
* **Easy scaffolding:** DMNO scaffolds your config schema based on existing `.env` files and references to env vars throughout your codebase.
* **Accessible config objects:** Just swap your calls from `process.env`/`import.meta.env` to the `DMNO_CONFIG` global that is automatically injected into your application. This gives you access to your type-safe config, helpful errors, and access to public+dynamic config items (if applicable).
**Astro-specific features:**
Most of these features are standard to DMNO, but there are a few Astro specific benefits and features:
* **Use env vars in astro config:** Easily access env vars directly in your `astro.config.*` files.
* **Middleware injection:** Automatically injects a middleware which detects leaked secrets in rendered responses as well as built js files being sent to the client. This middleware also warns if you accidentally use dynamic config items during pre-rendering of static pages.
* **Adapter support:** Full support for the Netlify and Vercel adapters, with more in the works.
### Get started
DMNO provides the most powerful way to manage configuration in Astro projects, all while enhancing security, and improving developer experience.
All it takes is `npx dmno init` 🎉
For more detailed information, visit the [DMNO Astro Integration Guide](https://dmno.dev/docs/integrations/astro/).
# Level up your env var tooling in Next.js with DMNO
We’re super excited to finally announce that our [Next.js integration](/docs/integrations/nextjs/) is ready - and we believe it is the BEST way to manage configuration in Next.js apps. Dealing with config is Next.js has usually been an afterthought - use `process.env`, maybe a `.env` file, and call it a day. But the whole experience is clunky and leaves you open to some major security risks.
Here’s a quick overview of the main problems and how DMNO solves them:
### 👷 Beyond type-safety
**Problem:** Environment variables are always plain strings, so you must coerce and validate them yourself (if at all). You also won’t get any types on `process.env` unless you manually add them, and you probably shouldn’t put a coerced non-string values back into `process.env` anyway.
**Solution:** DMNO lets you define a simple schema for your config. Aside from being more clear, this lets us do some magical things:
* Easily add **coercion and validation** to your config, so you are guaranteed the data is what you think it is
* Validates your config during your build and **before boot**, so you’ll know exactly what’s wrong before bringing down prod
* **Auto-generated TypeScript types** that match your schema, including detailed JSDoc comments explaining what each config item does


### 🔐 Effortless secure collaboration
**Problem:** Every time we onboard a new team member or add a new external service, we often need to share some set of secrets. You’ve *never* sent anything like that over slack… right?? Not to mention needing to keep all the various platforms we build, test, and run our applications on all in sync.
**Solution:** With your schema as part of your repo, you’ll never pull down the latest only to be met with unexpected crashes due to missing config. Inline documentation and validations let you know exactly what each config item does, and whether it is required. Plugins let you sync sensitive config securely, either [encrypted in your repo](/docs/plugins/encrypted-vault/), or with secure backends like [1Password](/docs/plugins/1password/). Plus unifying how config is managed makes it much easier to reason about and debug.

### 🤐 Leak detection
**Problem:** With the edges of client and server getting blurrier each day, it has become easier to accidentally leak sensitive config. Your secrets can end up in server-rendered pages and data, built javascript code, or be sent to the wrong 3rd party - especially logging and error tracking tools.
**Solution:** DMNO does [everything possible](/docs/get-started/security/) to protect you from accidental leaks. These features are opt-in but we think they are invaluable.
* DMNO patches global `console` methods to **scrub sensitive data from logs**
* Built client JS files and outgoing **server responses are scanned** for leaks before they are sent over the wire
* outgoing HTTP **requests are scanned** to make sure secrets can only be sent to whitelisted domains - e.g., only send your Stripe key to api.stripe.com



### 📐 Dynamic configuration control
**Problem:** The `NEXT_PUBLIC_` prefix controls whether config is public AND whether it will be static, meaning replaced at build time. If you want build once and deploy for multiple contexts, you’ll have to awkwardly wire up fetching the config on your own.
**Solution:** DMNO decouples the concepts of “sensitive” and “dynamic”, and supports both **sensitive+static** and **public+dynamic** config, and lets you set them explicitly in your config schema. This finer control is easier to reason about and we handle the hard part for you. See our [dynamic config guide](/docs/guides/dynamic-config/) for more details.

### 🌲 Unified config system
**Problem:** In a larger projects, especially monorepos, each part of your system ends up with its own hacked together config tooling, and there’s no way to share config across services. Keeping things in sync is error-prone and awkward, and dealing with many different tools is a pain.
**Solution:** DMNO lets you easily define config once and share it across your entire monorepo. Even outside of monoepos, using the same config system across your entire project will make your life much easier.

### ⛓️ Flexible value dependencies
**Problem:** Ideally we could set config values based on other values. While Next.js does have some built-in handling of multiple environment specific `.env` files (e.g., `.env.production`), this behavior is tied to the value of `NODE_ENV`. Since npm module installation behavior is also affected by this, often you’ll end up running your build with `NODE_ENV` as `development` even when doing a non-production build. You can also use [$ expansion](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#referencing-other-variables) to do some basic referencing, but it’s extremely limited.
**Solution:** DMNO’s config resolution logic allows you to reference other values however you like. You can define switching logic based on any value (not just `NODE_ENV`) and reuse values within arbitrary functions. Your configuration schema all forms a reactive DAG, which can also be visualized for simple debugging.

***
## So what’s *Next*
While much of what was described above is not specific to Next.js, a **TON** of specific work went into making sure our Next.js integration *just works* without additional setup on your part. We want this to become the default way that everyone deals with configuration in their Next apps, and for JS/TS in general. This stuff may seem like it’s not a problem, and day to day it may not be - until it bites you. Setting up your schema isn’t much harder than writing a `.env.example` file and you get SO much more, so please [give it a try](/docs/get-started/quickstart/), and let us know what you think!
Ready to give it a try?
Hopefully the benefits are obvious, and we know once you try out DMNO, you’ll love it 🥰.
Getting started is incredibly easy and it’s 100% free and open-source.
All it takes is `npx dmno init` 🎉
For more detailed information, visit the [DMNO Next.js Integration Guide](https://dmno.dev/docs/integrations/nextjs/).
# Announcing DMNO's Developer Preview
## Hello DMNO 👋
We (Theo and Phil), are happy to announce the Developer Preview of our new project, DMNO. Our first release is a configuration engine that helps you manage your configuration in a single place, and access it in a type-safe way across your services.
We’ve been heads down working on this project for a while now, and we’re excited to share it with you and let the community shape its future. While it’s still early days, we’re can’t wait to see what you build with it.
To make things easier to get started we’ve created a few integrations with popular frameworks and tools, and we’re working on more.
* [Astro](/docs/integrations/astro/)
* [Vite](/docs/integrations/vite/)
* [Next.js](/docs/integrations/nextjs/)
* [Node.js](/docs/integrations/node/)
We’ve also created a few plugins to help you securely manage your secrets and other sensitive configuration items.
* [Encrypted Secrets](/docs/plugins/encrypted-vault/)
* [1Password Secrets](/docs/plugins/1password/)
To get started you can check out our [quickstart guide](/docs/get-started/quickstart/), or dive into our [schema guide](/docs/guides/schema/).
We’ve also create a short video that goes over the basics of DMNO, and how it can help you manage your configuration. Check it out below:
## What’s next?
We will continue to build out more integrations, and evolve our configuration engine in response to your feedback.
In addition to that, we have two big products on the horizon:
* **DMNO Dev**: A UI-driven way to run your full stack apps locally, powered by DMNO’s configuration engine.
* **DMNO Deploy**: A way to deploy your apps to your existing platforms, powered by DMNO’s configuration engine.
Our goal is to make it as seamless as possible for you to configure, run, and deploy your *entire stack* and we’re just getting started!
# Safer application secrets with 1Password and DMNO
## Introduction
If you’ve ever felt that sinking feeling when dealing with sensitive environment variables then this post is for you. We’ll show you how we can take them from a ‘set it and forget it until it comes back to haunt you’ concept to something that will provide confidence and safety throughout your entire development workflow.
*But first, a short history lesson.*
### A very brief history of environment variables
Believe it or not, the humble environment variable (env var) dates back all the way to Unix Version 7 in 1979! They’ve been around longer than DOS and Windows, which quickly adopted their own notion of env vars after they came onto the scene in the 80s. Env vars exist to provide arbitrary values to running processes, and their children. They are the de facto method for storing application configuration such as secrets, feature flags, credentials, etc.
They have been largely unchanged since they appeared 46 (!) years ago. One more recent advancement was the introduction of the `.env` file by Heroku in 2012, used to populate their Config Vars, which were then injected into the environment during deployment. These `.env` files were also a means of conforming to the [12 Factor](https://12factor.net/) standard that Heroku’s engineering team championed. The `.env` file did a lot to help but it also created some unforeseen challenges.
### Why your .env is failing you
Because `.env` became the standard, it has also devolved into a pseudo-onboarding tool. Despite knowing it’s dangerous from a security perspective, it is extremely common to share a `.env` file full of secrets with the new hire on Slack. Even when sharing values securely, it is often a manual one-time process, and that `.env.example` file which serves as a template and documentation for required config can quickly get out of sync with the real `.env` that it’s trying to mirror.
`.env` has also become an incomplete picture of what configuration is. Because of the headaches involved with collaboration, `.env` typically contains only sensitive or secret items, and it means the non-sensitive items tend to live elsewhere in the code. Doesn’t it seem odd that you get two keys from Stripe, but the public and private keys are defined in two different locations?
Finally, because of the nature of env vars, everything is stored as a string. This means that it’s often left to the developer to coerce values into their actual types (number, object, arrays, etc), to validate that values are populated and valid, and ideally to have type-safety for those values.
It’s not to say that there aren’t a whole host of tools out there to deal with this, but what that typically looks like is cobbling together several of them in hopes that they solve all of the above. In a larger project, this often means using an entirely different setup for each part of your stack, with no easy way to share config across different parts of your system.
Not to mention, that even in a good-enough `.env` setup, chances are it’s still sitting on your machine in plaintext!
*There has to be a better way, right?!*
## A more modern approach
After speaking with a cross-section of development teams across many stages and sizes, we realized that most of them were storing sensitive information like API keys and other credentials in 1Password and then copy and pasting those into `.env` files, or into the GUIs on their platform of choice for deployments. And although 1Password has a few developer-centric methods for managing the items stored in your vaults (e.g., SDKs, CLI), actually using that in your code is an exercise left to the reader.
So given the flexible nature of DMNO, it was an obvious choice for one of our first plugins to allow the retrieval of items from 1Password. In addition the wealth of features DMNO already provides, with our 1Password plugin you can also:
* Secure your entire existing `.env` files in a single 1Password item - i.e., secure the entire file with a single copy and paste
* Segment config into multiple vaults per environment, service, or however makes sense for your application
* Use the 1Password app’s biometric unlock to secure your secrets when developing locally
* Or: use with no dependencies other than the plugin via a service account token - using the 1Password SDK under the hood
If your team is using 1Password today and duplicating secrets or writing a bunch of custom code just to sync them, then DMNO will make your life much easier and more secure.
To get started see our [1Password plugin guide](https://dmno.dev/docs/plugins/1password/).
Or read on for a full tutorial.
First let’s assume you have `.env` file that looks something like this:
```bash
API_SERVER_URL=”api.myserver.com”
API_SERVER_PORT=”4392”
DB_USER=”prod-user”
DB_PASS=”prod-secret”
DB_PORT=”5432”
DB_URI=”mydbserver.com”
DB_CONNECTION_STRING=”pgsql://$DB_USER:$DB_PASS@$DB_URI:$DB_PORT”
GITHUB_TOKEN=”ijfdpaojifdipajifjdp2144oajfpijapijfdoiaj123opiijfdiopiajf”
STRIPE_TOKEN=”pk_ipjfdioajp55fidjiaopjiofd_ijjpofd14oiajfpodj”
```
Chances are this most resembles your production environment and you’ve got a completely different version locally, although some of the individual items might be the same. We’ll address this later, but just note that this .env *drift* is sort of inevitable with setups like this.
## Setup
A note on requirements:
* You’ll need Node (>20)
* A js package manager (npm, pnpm, etc)
* Linux/OSX/WSL
### 1Password
DMNO’s integration with 1Password makes use of Service Accounts. So you’ll need to create one that has access to the Vault that will hold your sensitive config items.
> Note that you **cannot** change the access of a particular service account after it has been created.
For the purposes of this tutorial, let’s create a new Vault called **DMNO Production Secrets**, and create a service account that has access to only this vault. See the 1Password docs for information on creating Vaults and Service Accounts.
### The local option
Since the easiest way to interact with 1Password on your local machine is their desktop application, let’s get that set up. This has the added advantage of allowing you to use biometric authentication with our plugin. \*\*You only need to do this if you want to use the local application with the integration while working your local machine. If you just want to use service accounts, you can skip this step. \*\*
If you haven’t already, do the following:
* Install 1Password’s desktop application
* Install the 1Password CLI (used to communicate with the application locally)
* Turn on the CLI integration in the Developer Settings (see screenshot below)

*Check the ‘Integrate with 1Password CLI\` box*
### DMNO
In the root of your project, or the package/service in question, run the following:
```bash
npx dmno init
# follow the prompts and after that is all complete
npm add @dmno/1password-plugin
```
After all of that you should have one, or more. `.dmno/` folders. We’ll assume a single folder for now for brevity’s sake.
In your `.dmno/config.mts`, let’s add the code for the 1Password plugin:
```typescript
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
// token will be injected using types by default
const onePassSecrets = new OnePasswordDmnoPlugin('1pass', {
fallbackToCliBasedAuth: true,
});
export default defineDmnoService({
name: ‘my-service’,
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
// NOTE - the type itself is already marked as sensitive 🔐
},
},
});
```
In this code we’re importing the required parts from our plugin, creating an instance of the plugin and adding a schema item, in this case a sensitive one, that holds our service account token. You’ll notice we’re not explicitly passing the token to the plugin when we instantiate it. This is because with DMNO’s smart type system we know that we have a `serviceAccountToken` available to us so we can automatically inject it.
Additionally, because we have `fallbacktoCliBasedAuth` enabled, when no service account token is found, we’re relying on the CLI to interface with the 1Password desktop app – bypassing the service account entirely. This allows us to avoid passing around any auth tokens and use the additional biometric security provided by 1Password on your local machine. In a deployed environment, like in CI/CD, it will still use the service account you set up.
### What about blob?
Let’s return to our blob-style `.env` file and secure it with 1Password. With everything that we’ve set up this should mean just a single copy paste and a few updates to our DMNO schema.
In the **DMNO Production Secrets** vault that you created, create a new Secure Note, and give it an appropriate name, such as “Prod secrets”.
In that item create a new multi-line text field, and change the label of that field to match your service name - which will be the “name” field of your package.json file or the `name` you used in your `config.mts`. In the example above, this is `my-service`. If you’ll only ever have a single service you can use the special `_default` name.
Now paste in the contents of your `.env` file from above.

*Your vault should look something like this*
### Wire it up
Now all that’s left is to update our schema items to fetch from 1Password.
```typescript
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
// token will be injected using types by default
const onePassSecrets = new OnePasswordDmnoPlugin('1pass', {
fallbackToCliBasedAuth: true,
envItemLink: ‘https://start.1password.com/open/i?a=I3GUA2KU6BD3IJFPDAJI47QNBIVEV4&v=wpvutzohxcj6kwstbzpt3iciqi&i=ti3v3j3fjdaipofj4ejlr373vdivxi&h=mydomain.1password.com’
});
export default defineDmnoService({
name: ‘my-service’,
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
// NOTE - the type itself is already marked as sensitive 🔐
},
// include as many items as you want to fetch from your 1Pass .env item
// we’ll include two for brevity’s sake
GITHUB_TOKEN: {
required: true,
sensitive: true,
value: onePassSecrets.item(),
},
STRIPE_TOKEN: {
required: true,
sensitive: true,
value: onePassSecrets.item(),
},
},
});
```
First, we add the `envItemLink` to the plugin initialization which tells it to look in a particular item for the env blobs. Then, for each schema item (wired up via the `.item()` method) it will look in the `my-service` entry for a key that matches and securely load the value. Finally, we’ve added the `required` and `sensitive` so that DMNO can use appropriate validation and security rules for the items.
### Improved DX
Now in your application code you can use the DMNO\_CONFIG globals to reference config items and benefit from improved type-safety and Intellisense.
```typescript
const GH_TOKEN = process.env.GITHUB_TOKEN;
const GH_TOKEN = DMNO_CONFIG.GITHUB_TOKEN;
```

And, naturally, you will also benefit from all the additional features that DMNO provides including: validation, coercion, leak prevention and detection, and log redaction – to name a few.

## What next?
To recap, you now have env vars stored securely in 1Password. You’re using biometric authentication when developing locally and service accounts everywhere else. Your items are now type-safe, validated, and kept in sync automatically.
Things are feeling good. 😎
Next, you may want to:
* break out your blobs into individual items in 1Password ([read more](https://dmno.dev/docs/plugins/1password/#using-specific-1password-items)).
* have multiple vaults with differing levels of access (e.g., one for dev and one for prod). ([read more](https://dmno.dev/docs/guides/secret-segmentation/))
* Add further validation and documentation to the individual items ([read more](https://dmno.dev/docs/guides/incremental-adoption/))
Feedback
Is there something that we missed or another feature you’d like to see?
Drop us a line on [Discord](https://chat.dmno.dev), your feedback is important to us.
# Legal
## License
DMNO Config is licensed under the [MIT License](https://opensource.org/licenses/MIT). For full details, please see the [LICENSE](https://github.com/dmno-dev/dmno/blob/main/LICENSE) file in our GitHub repository.
## Disclaimer
DMNO Config is provided “as is” without warranty of any kind, express or implied. Use at your own risk. The authors or copyright holders shall not be liable for any claim, damages, or other liability arising from, out of, or in connection with the software or the use or other dealings in the software.
## Privacy Policy
Our website may collect anonymous usage data to improve our services. This data is collected using third-party analytics tools and may include:
* IP addresses (anonymized)
* Browser type and version
* Operating system
* Referring/exit pages
* Date/time stamp
* Clickstream data
We do not collect personally identifiable information unless explicitly provided by you (e.g., when contacting us via email).
## Cookie Policy
Our website may use cookies to enhance your browsing experience. You can choose to disable cookies in your browser settings, but this may affect some functionality of the site.
## Changes to Legal Policies
We reserve the right to update these legal policies at any time. We encourage you to periodically review this page for the latest information on our legal practices.
## Contact Us
If you have any questions about these legal policies, please contact us at .
# Concepts
> Learn the core concepts of DMNO
DMNO is built on familiar concepts, but naming is hard and consistency in terminology is important - so we want to help clarify what we mean with a few terms:
## DMNO Concepts
### Workspace
The top level folder of your DMNO project. Usually this aligns with the root of your git repository itself.
In the single repo case, your workspace is made up of a single package.
In the monorepo case, this corresponds to the “workspace” concept of pnpm or the “workspace root” in yarn/npm.
Caution
Confusingly, yarn and npm use the term “workspaces” when specifying where the child packages are located.
We try to follow pnpm’s lead and refer to the whole thing as the “workspace” made up of “workspace packages”.
### DMNO Service
A package in your workspace that uses DMNO - and has a `.dmno` directory. This is usually a runnable/deployable chunk of your system (ex: database, api, website) but could be any package that uses config / env vars. Usually every package in your monorepo will be a dmno service except for simple shared libraries (e.g., shared eslint config, shared types). Sometimes, you may also have services that are purely used for grouping and to define shared config.
Examples of packages likely to be DMNO services:
* `@my-org/api`
* `@my-org/website`
Examples of packages likely to NOT be DMNO services:
* `@my-org/eslint-config`
* `@my-org/shared-types`
### Root Service
Every DMNO workspace must have a single root service.
In the single-repo case, your workspace is made up of a single service, which is the root service.
In the monorepo case, this will be a single service that lives at your workspace root and is the default parent of all other services. In this case you wouldn’t think of this service as a runnable chunk of your system, but instead as a place to define default settings inherited by other services and config to be shared across your whole system.
### Config Schema
Each DMNO service defines a configuration schema which is made up of many config items. Each item has a key and a data type - which defines things like validation and coercion logic, documentation, whether the item may be sensitive, and sometimes logic about how to set the value.
Your config schema should define the full shape of all the environment variables used by the service and items that may be defined by the service for use by other services.
### DMNO Data Type
DMNO has its own type system that covers validation, coercion, documentation, and even rules about how to set values. This type system also has an inheritance mechanism so types `extend` each other to form a chain of ancestors.
Our [`DmnoBaseTypes`](/docs/reference/base-types/) are factory functions that create an instance of a `DmnoDataType` and apply specific settings. For example: `DmnoBaseTypes.number({ min: 0, max: 100, precision: 0 })`.
To get actual properties from the type instance, in most cases we walk up that chain of ancestors until a value is found. In a few cases we may apply multiple values found on the ancestor chain - like merging multiple external docs links, or applying multiple validation functions.
Our data types can be used to generate extremely rich TypeScript types, and soon we will be able to generate types for other languages as well.
### Pick
In a monorepo project, services can reach into a parent or sibling to pick config items - and optionally transform keys and/or values along the way. This lets us easily define and reuse shared config items, and picking from siblings also gives us an implicit dependency graph of how our services are related.
### Resolution (resolve, resolvers)
DMNO config is loaded in 2 phases - first we load the schema itself, and then we attempt to resolve the values. This resolution process calls special resolver functions and passes in extra contextual information about the item being resolved and the rest of the resolved configuration values. This lets us form a reactive [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) to generate our configuration, and to understand much about the shape of that graph without necessarily needing to know the values themselves.
### Overrides
Your config schema may define how to resolve a value for a config item - but this value can always be overridden by a value coming from file-based overrides (`.env` files or similar) or from actual environment variables in your shell. A single item could have multiple overrides present and there is a precendence order that they are applied in. Being able to see all of these values and which one is currently active will save you tons of headaches.
For example, in order of least to most precedence:
* value from schema
* value from `.env`
* value from `.env.local`
* value from environment variable
### Plugin
DMNO plugins are packages that extend the functionality of DMNO itself. This could be anything from secrets backends to additional bundles of custom types. DMNO’s open nature means this ecosystem of plugins will only continue to grow and evolve.
### Integration
DMNO integrations are packages that allow you to more easily use DMNO with other frameworks and tools. They sometimes have a different name within that tool, like “plugin”, but these packages should include everything you need to easily integrate DMNO into that system, whether that be a plugin for that tool, helper functions, etc.
### Platform
DMNO platform packages are meant to provide everything you need to easily use dmno on a specific infrastructure platform. This could be a collection of data types, a pre-built config schema of all the env vars that platform injects into your app, plugins for their tools, or helper functions.
Often you will not need any special platform integration to use dmno on a platform, but it depends on the platform and which of their features you may be using.
## Related Terms
### ”Monorepo” vs “Single project repo”
A **“monorepo”** is a technique where multiple related projects are developed from a single git (or other VCS) repository. DMNO is particularly useful in monorepos because sharing config across projects within a monorepo is hard. There are many reasons why working in a monorepo can be extremely beneficial, but many teams shy away from them because setting things up properly is hard. DMNO is aiming to fix that!
We use the term **“single project repo”** or sometimes **“single repo”** to denote the opposite kind of repository - one that contains only a single project. Although in larger systems this could also be part of a multi-repo strategy, where you have many related single project repos that make up your system.
# Quickstart guide
> Get started with DMNO and start managing your configuration more effectively.
## Current requirements
DMNO Config requires either of the following:
* `node` (>22.x) + either `pnpm`, `npm`, or `yarn`
* `bun`
Tip
While TypeScript is not required in your applications, you will get the full feature set (e.g., IntelliSense and inline docs) of DMNO by using it in a TypeScript project. Note that our `config.mts` files themselves are written in TypeScript, so you’ll want to use an editor that at least supports it.
1. ### **Setup `dmno` in your project**
Run this command in the root of your project:
* npm
```bash
npx dmno init
```
* pnpm
```bash
pnpm dlx dmno init
```
* Yarn
```bash
yarn dlx dmno init
```
* Bun
```bash
bunx dmno init
```
This will create a `.dmno` folder in the root of your project with a `config.mts` file, including config items in your `schema` that we automatically scaffold using config items we find in `.env` files and in your source code. If in a monorepo, any additional services of your choice will get their own `.dmno` folders and associated files. Each `.dmno` folder looks something like this:
* /your-project
* .dmno
* .typegen/ (generated types)
* …
* .env.local (optional local overrides file, gitignored)
* **config.mts** (your config schema)
* tsconfig.json (dmno specific tsconfig)
* … the rest of your files and folders
2. ### **Run `dmno resolve` in watch mode**
This will give you instant feedback while you author your config schema.
* npm
```bash
npm exec -- dmno resolve -w
```
* pnpm
```bash
pnpm exec dmno resolve -w
```
* Yarn
```bash
yarn exec -- dmno resolve -w
```
* Bun
```bash
bun run dmno resolve -w
```
3. ### **Write your schema**
The config schema and other settings live in the `.dmno/config.mts` files. `dmno init` does its best to scaffold out the initial version of this schema but it should be reviewed. Updating each item with a description, [`required`](/docs/guides/schema/#validation), and [`sensitive`](/docs/guides/schema/#sensitive) is a great next step. You can then improve your schema over time, adding validations, and setting values from within the schema itself.
Your initial schema should look something like this:
.dmno/config.mts
```typescript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
schema: {
PUBLIC_API_BASE_URL: {
extends: DmnoBaseTypes.url,
description: 'Base URL for the public API',
},
PUBLIC_GOOGLE_ANALYTICS_ID: {
description: 'Google Analytics ID',
},
DATABASE_URL: {
sensitive: true,
extends: DmnoBaseTypes.url,
description: 'Database connection string',
},
SECRET_API_KEY: {
sensitive: true,
},
JWT_SECRET: {
sensitive: true,
},
},
});
```
Check out the [schema guide](/docs/guides/schema/) for full details.
And if you would like more information on how we use `.env` files check out [\`.env’ file guide](/docs/guides/env-files/)
4. ### **Configure framework specific integrations**
We provide [drop-in integrations](/docs/integrations/overview/) for many popular frameworks, and more are in the works. `dmno init` is smart enough to install the relevant integrations for each service. You can also read more about each integration on their respective [pages](/docs/integrations/overview/) and update them as needed.
In this case of [Astro](/docs/integrations/astro/) or [Vite](/docs/integrations/vite/), the integrations should work out of the box. In other cases, like [Node.js](/docs/integrations/node/#watch-mode-and-dev-commands) or [Next.js](/docs/integrations/nextjs/#adjusting-packagejson-scripts), you will need to update your `package.json` scripts to use [`dmno run`](/docs/reference/cli/run/) so that your resolved config is passed to the script in question.
5. ### **Use `DMNO_CONFIG` to access your config**
We recommend migrating to `DMNO_CONFIG` as it provides helpful improvements like TypeScript autocompletion and IntelliSense.
For example:
```ts
// 😿 still works, but no type-safety, and will be a string
if (!process.env.SOME_NUMBER) {
throw new Error('Missing SOME_NUMBER env var');
}
const myConfigNum = parseFloat(process.env.SOME_NUMBER);
// 🎉 easier, safer, full type-safety
const myConfigNum = DMNO_CONFIG.SOME_NUMBER;
const IS_PROD = DMNO_CONFIG.NODE_ENV === 'production';
```
You *could* continue to use `process.env`/`import.meta.env` to access your config and still benefit from DMNO’s validation logic. But, `DMNO_CONFIG` gives you the full benefits of DMNO.
Secrets & DMNO\_PUBLIC\_CONFIG
While building code for the browser or another less-secure context, you can use the `DMNO_PUBLIC_CONFIG` object - it does not contain any items that are marked as `sensitive: true` in your schema
# Schema Sample
# Security
> DMNO is designed to keep your configuration safe and secure while maintaining simplicity and developer experience.
Secrets are special
One of the things that sets DMNO apart from other configuration and secrets management solutions, is that it doesn’t force you to treat secrets differently from the rest of your config. That doesn’t mean they aren’t *special* though.
Not only does DMNO offer you ways to manage your secrets via [plugins](/docs/plugins/overview/), but it also provides the guardrails in its core libraries to make sure they don’t leak and to make sure things are encrypted when they need to be.
We built DMNO with security in mind from day one. As such, there are many security related features built right into the platform itself:
* All caches are encrypted
* We provide a global `DMNO_PUBLIC_CONFIG` object that only includes *non-sensitive* items
* We prevent secrets from being displayed in the console wherever possible
* We provide [plugins](/docs/plugins/overview/), to securely store and retrieve your sensitive configuration, with minimal effort
Plus we have a few opt-in features to protect you from accidentaly leaking config:
* **Leak detection** - scan built code and data for leaks before sending to the client
* **Log redaction** - hide sensitive data in console output
* **HTTP request interception** - ensure sensitive data is only sent to domains you specify
Read on below for more details about each of these features.
Secrets are still secrets
That said, if someone has access to your running source code, then they have access to your secrets. So, design your systems accordingly. Zero trust is always the best approach.
### Leak detection
Wherever possible, our integrations will inject logic to help protect you from accidentally leaking sensitive config. This could mean scanning built javascript code that is bound for the client or scanning server-rendered responses. For some integrations this means hooking in the build system (e.g., vite, webpack, etc), injecting a middleware, or injecting additional code in built javascript files that run on the server. Each integration is different, but we try to make it as simple as possible.
Leak detection can be enabled using the `preventClientLeaks` service setting. For example:
.dmno/config.mts
```ts
export default defineDmnoService({
settings: {
preventClientLeaks: true,
},
//...
```
Service settings are inherited
In a monorepo, each service setting value is inherited from parents if no value is found on the service itself. So in a monorepo, you can enable it in your root service to enable it in all of your services.
Some integrations may not allow us to inject this functionality automatically, but we do our best. The docs for each integration will let you know if any additional setup is required or not.
### Log redaction
Whenever DMNO itself is logging a sensitive value, we will redact (hide) the full value. For example `secret123` may be shown as `se▒▒▒▒▒▒▒`. However, we take this one step further for Node.js applications by providing functionality to patch the global `console` methods to redact the values any time they would appear in logs. The replacement is based on the value itself, so it does not matter *how* the value ended up in the log. Depending on where you host your applications, these logs are often sent to 3rd party services, so keeping them out of your logs is more important than you might think.
This feature can be enabled using the `redactSensitiveLogs` service setting. For example:
.dmno/config.mts
```ts
export default defineDmnoService({
settings: {
redactSensitiveLogs: true,
},
//...
```
See [schema guide > security](/docs/guides/schema/#redact-mode) for more details about customizing redaction behaviour.
### External HTTP request scanning
Similarly, we provide functionality to patch node’s global http internals so that we can snoop on all outbound requests and make sure your sensitive config is only sent where is supposed to go. This is particularly helpful to make sure you don’t accidentally send secrets to a logging or exception tracking service.
This must be enabled using the `interceptSensitiveLeakRequests` service setting, and sensitive config schema items must have have an `allowedDomains` list set.
.dmno/config.mts
```ts
export default defineDmnoService({
settings: {
interceptSensitiveLeakRequests: true,
},
schema: {
STRIPE_SECRET_KEY: {
sensitive: true
sensitive: {
allowedDomains: ['api.stripe.com']
}
}
}
//...
```
Note that when using published reusable types, they will often have the correct `allowedDomains` list set properly already.
# What is DMNO?
> DMNO is a suite of developer tools that allow you configure your applications with type-safe, schema-driven environment variables.
DMNO (👂 “domino”) is a suite of developer tools that allow you configure, run, and deploy your applications. We help you connect the dots from local development all the way to production.
At the core is **DMNO Config** - the best way to deal with configuration / environment variables in your code. By defining a full schema of all the configuration used throughout your entire system, it provides:
* a unified system to manage your config across *all* of your services
* validations, coercion logic, and full type safety
* built in documentation, and magical IDE auto-completion
* secure handling of your secrets, including leak detection and prevention
* ability to sync secrets with various backends (e.g., [1Password](/docs/plugins/1password/)) or [encrypt](/docs/plugins/encrypted-vault/) them within your repo - *no more insecurely passing around `.env` files via Slack* 🎉
* drop-in [integrations](/docs/integrations/overview/) to use with your favorite frameworks
* extensible type and plugin system to customize behavior
While this tool is in itself incredibly useful and usable on its own, it also forms the foundation for further tools to help you run your applications locally during development (**DMNO Dev**) and deploy them to the cloud (**DMNO Deploy**).
## Who is DMNO Config for?
We designed DMNO for everyone
From a single developer working in a monolith to a huge team wrangling a sprawling monorepo.
* **Solo developers** - who want the added safety and convenience of a full schema and validation system for their config.
* **Teams** - who want to accelerate development work by avoiding entire classes of bugs related to config, and have a unified system to manage, and share their config across all of their services.
The short answer is: anyone who uses environment variables to configure their applications - which, at this point, is basically everyone! Initially those who write TypeScript or JavaScript will benefit most, but almost anyone will benefit from DMNO’s schema and validation system and it is specifically designed to handle polyglot use cases. Additionally, while working in a monorepo is by no means a requirement to use DMNO, it specifically alleviates some of the problems involved with dealing with config in a monorepo setting.
We hope DMNO will become the default way to deal with config within JavaScript/Typescript - but we have no plans to stop there!
## When should I use DMNO Config?
* **When you’re tired of managing `.env` files** - DMNO provides a better way to manage your environment variables, including a full schema, validation, and coercion logic.
* **When you’re tired of debugging config issues** - DMNO provides actionable error messages so you can avoid entire classes of config-related bugs.
* **When you’re tired of passing around `.env` files** - DMNO provides a secure way to handle your secrets, including leak detection, prevention, and encrypted storage.
* **When you’re tired of writing the same config logic over and over** - DMNO provides a unified system to manage your config across *all* of your services.
* **When you’re tired of not having documentation for your config** - DMNO provides built in documentation, and magical IDE auto-completion.
* **When you’re tired of not having type safety for your config** - The rest of your code is typed, why not your config?
* **When you’re getting started on a new project** - DMNO is a great way to start a new project, as it will help you avoid common pitfalls and provide a solid foundation for your config.
## Why use DMNO Config instead of \_\_\_\_ ?
TL;DR, DMNO Config is:
* Strongly typed
* Schema-driven
* Extensible (via custom types and plugins)
* Incrementally adoptable (use as many or as few features as you like)
We realize there are no shortage of tools out there to help manage your environment variables. And for most people, it’s a set it and forget it type of thing. But even if it’s not top of mind at the moment, we guarantee it comes up *regularly* - like each time you: onboard a new team member, set up a new internal service, add a new external SaaS, or when someone forgets to update some API key in 12 different external systems.
Or maybe you think you don’t need a tool to help manage this stuff at all. We get it - and that’s why we know we must build a tool *an order of magnitude* better than the alternatives, and that’s exactly what we’re doing.
We aim to make your config the bedrock of your stack, which will vastly improve your ability to debug configuration and avoid entire classes of bugs. A full schema of your config will also unlock some very interesting things in the not-too-distant future.
## Current requirements
DMNO Config requires the following:
* Node.js LTS (currently v22.x)
* One of the following package managers:
* `pnpm`
* `npm`
* `yarn`
* If working in a monorepo, we currently support:
* pnpm workspaces
* npm workspaces
* yarn workspaces
Optionally:
* TypeScript
Tip
While TypeScript is not required in your applications, you will get the full feature set (e.g., IntelliSense and inline docs) of DMNO by using it in a TypeScript project. Note that our `config.mts` files themselves are written in TypeScript, so you’ll want to use an editor that at least supports it.
# Custom Types
# Dynamic vs static config
> Learn how to manage dynamic and static config items in DMNO in your static, server-rendered, or hybrid app.
If you do any front-end development, you’re probably used to the concept of replacing references to env vars with actual values *at build time* (e.g., [vite](https://vitejs.dev/config/shared-options.html#define), [webpack](https://webpack.js.org/plugins/define-plugin/)). This is useful so that:
* the client doesn’t have to fetch config before using it
* bundlers can drop unreachable code and dependencies
In most frameworks and build tools, this concept is tightly coupled with something being ***public***, and is triggered via a special prefix (e.g., `NEXT_PUBLIC_`) but can also be dependent on *where and how* you access the config, especially now that server-side/hybrid rendering is gaining popularity over the totally static JAMStack sites of the last decade.
### Schema to the rescue
In DMNO, rather than relying on name prefixes and tightly coupling the concepts of being secret with being static, we split them and give you explicit control which is easier to reason about:
* Is the config item *sensitive* (`sensitive: true`)
* Is the config item *dynamic* (`dynamic: true`)
Then in your code, use `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG` and we take care of the rest, including support for **static+secret** and **dynamic+public** config items! We’ll also do our best to help you detect when something unexpected is happening like pre-rendering a dynamic config item, effectively freezing its value on *some* pages, to help prevent errors and confusion.
### Default dynamic behavior
How items are treated by default, with respect to being dynamic, is something that likely depends on the kind of app/service you are building and how it will be deployed. So, we let you control this default behaviour with the `settings.dynamicConfig` property in your service config.
The following table shows the different modes supported:
| value | description |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| `public_static` ⭐ *default* | **non-sensitive = static, sensitive = dynamic** *use `dynamic: true \| false` option to override* |
| `only_static` | **everything static, dynamic not supported** *useful for static/SSG sites* |
| `only_dynamic` | **everything dynamic, static not supported** *useful for a backend app and not using any bundler* |
| `default_static` | **default is static** *use `dynamic: true` to override* |
| `default_dynamic` | **default is dynamic** *use `dynamic: false` to override* |
An example service schema using the `dynamicConfig` service setting and item overrides:
```ts
export default defineDmnoService({
settings: {
dynamicConfig: 'default_static',
},
schema: {
PUBLIC_STATIC: {},
PUBLIC_DYNAMIC: { dynamic: true },
SECRET_STATIC: { sensitive: true },
SECRET_DYNAMIC: { sensitive: true, dynamic: true }
}
})
```
Default behavior
The default dynamic config mode is `public_static` which matches what you are probably used to with other tools - sensitive items are dynamic and non-sensitive items are static.
Service settings inheritance
Service settings are inherited up through the chain of parent services if left unspecified.
### Fetching dynamic config on the client
To support accessing dynamic (i.e., non-sensitive) config in the client, we have to fetch it from the server, and in order to use the same access pattern (i.e., not make every call to get config async like `await getConfigItem('SOME_KEY')`) we use a *blocking* http call. This means you should use it sparingly, if at all, and probably not on page load. That said, it’s not that different from making an additional blocking JavaScript request, and we can do some fancy tricks to minimize the impact. We’re working on further tools and options around this in each integration, but it will likely be opt-in or triggered on-demand only as needed.
A totally pre-rendered static build will not support dynamic config, and some integrations may require a few steps to wire up the endpoint that exposes these config values.
### Static secrets
In most cases you won’t need static secrets. The main use case would be to take advantage of tree-shaking, which could be helpful to create a smaller/faster bundle. That said, it’s just another tool in your toolbox - we’re excited to see how you use it.
Caution
Bundling sensitive config items into your built artifacts could be a security issue depending on how and where your code is deployed, although in many cases it’s totally fine. Think carefully about where your built artifacts live and who has access to them.
# How DMNO uses .env files
> Learn how DMNO uses `.env` files to scaffold out your config schema and set value overrides.
DMNO uses `.env` files in two ways:
1. To scaffold out the initial [`schema`](/docs/guides/schema/) in your `config.mts` files when you run `dmno init`
2. To set value [overrides](/docs/guides/overrides/) to be used while resolving your config values
Let’s zoom in on each of these.
## Scaffolding the schema
When you run `dmno init`, DMNO will look for all `.env` files in your project and use them to scaffold out the `schema` in your `config.mts` files. This includes *all* `.env` files, regardless of being gitignored or checked in, including samples, and environment specific files.
We try to infer as much about each config item as possible by:
* Including related comments as a `description`
* Infer the type (`extends`) based on the value for basic types like `boolean`, `number`, `email`, `url`
* Set the `value` and not mark it as `sensitive` if the file was checked into source control. This includes using `value: switchBy('NODE_ENV', ...)` if values were found in multiple environment specific files
* Set an `exampleValue` from a value from a `.env.sample`
This automatic scaffolding of your config `schema` is meant to be a good starting point, but you should review it and make adjustments as necessary.
Note
`dmno init` will also prompt you to delete the `.env` files that were previously checked into source control because the values in those files have now been incorporated into your new config schema.
## Setting overrides via `.env` files
By default, DMNO enables the `dotEnvFileOverrideLoader` to load values from `.env` files. You can read more about the general concept of overrides(/docs/guides/overrides/) and how to adjust that behavior in the [Overrides guide](/docs/guides/overrides/).
If you don’t disable this behavior, dmno will load any `.env` files within a `.dmno` folder and apply those values as *overrides*. These values will take higher precendence than values set from your `schema`, and lower precedence than overrides set via actual environment variables passed in from your shell. The overall precedence order from highest to lowest is:
* Environment variables from your shell (e.g., `ENV_VAR=xyz npm run dev`)
* File based overrides
* `.env.*.local` - applied only if `NODE_ENV` matches `*`
* `.env.*`
* `.env.local`
* `.env`
* Values set via your `config.mts` schema
Environment scoped files and NODE\_ENV
To decide which `.env.*` files to enable, we use the current value of `NODE_ENV` that we find in actual environment variables via `process.env`, not any value set via your schema or another `.env` file.
While we support it, **we do not recommend using complicated `.env` file setups like this within dmno!** It is supported purely to ease migration. Instead we recommend migrating this logic into the schema itself - setting values using functions and helpers like `switchBy` to express more complex overriding behaviour, and using plugins to load sensitive values securely. See our [Schema Authoring guide](/docs/guides/schema/#value) for more details.
We do, however, recommend using a single gitignored `.env.local` file to store any overrides you want to apply locally. This can be useful for short-lived temporary settings that you may want to toggle during development - like flags that enable certain debugging related features. It’s also where we recommend storing sensitive keys that you don’t want checked into version control; values that in deployed environments you would set via actual environment variables.
If you’re using [plugins](/docs/plugins/overview/) to handle your sensitive config values, you would store the sensitive keys that enable those plugins in your `.env.local` file. For example, for our [1Password](/docs/plugins/1password/) plugin, the 1Password service account token, or for our [Encrypted Vault](/docs/plugins/encrypted-vault/) plugin, the key used to decrypt the vault file. **This allows you the simplicity of only having to worry about one single config item, while keeping everything secure.**
Tip
When running `dmno init`, we prompt you to move any gitignored `.env` files we find into your `.dmno` folder. This means that other tools that may be looking for will not find them - which is on purpose. Instead, you should pass resolved config to those external tools via `dmno run`, whether `.env` files are being used or not.
## Resolving config and outputting to .env format
You can use the [`dmno resolve` command](/docs/reference/cli/resolve/) to load the resolved config for a service and output it in `.env` file format. This is useful for quickly exporting all your config values to a file for use in other systems, especially in some serverless environments where you may need to set a lot of environment variables at once and you don’t have as much control over the running process as you do locally.
Consider the following example where we want to load an `.env` file for use in Supabase Edge Functions.
You can run the following command to load the resolved config for your `api` service and output it to a file.
```bash
pnpm exec dmno resolve --service api --format env >> .env.production
supabase secrets set --env-file .env.production
```
If you want to do this with a single command, you can combine them like this:
```bash
supabase secrets set --env-file <(pnpm exec dmno resolve --service api --format env)
```
This has the added benefit of writing no file, so you don’t need to worry about deleting it later or accidentally checking it into source control.
# Incremental adoption
> DMNO allows you to adopt it incrementally, both in terms of the services you use it with and the features you use within those services.
Because of the way DMNO is designed you can adopt it incrementally, both in terms of the services you use it with and the features you use within those services.
## A few different options
There are a few different ways you can adopt DMNO incrementally:
**Service by service**:
* You can start by adding DMNO to a single service, and then gradually add it to more services as needed. If you’re in a monorepo, it’s best to start with some root config and then incrementally add a service at a time.
**Feature by feature**:
* Mark items as `required` and [`sensitive`](/docs/guides/schema/#sensitive)
* Add descriptions to items to [automatically attach documentation](/docs/guides/schema/#docs)
* Add [specific types](/docs/guides/schema/#data-type) to each of the config items
* Then add custom [validation rules](/docs/guides/schema/#validation), coercion, and custom types
* Set [values](/docs/guides/schema/#value) from the schema itself (e.g., functions, switching [based on environment](/docs/guides/multi-env/))
* Add [plugins](/docs/plugins/overview/) to fetch the config values from a secure source
**In your code**:
* We will support your existing methods of using env vars (e.g., `process.env` in Node.js, `import.meta.env` in Vite, etc) while you migrate to DMNO, by reinjecting your resolved config items as strings. You won’t get type completion, full IntelliSense, and you won’t get an error if you try to access a variable that doesn’t exist, unfortunately.
* Once you’ve added DMNO to your project, you can start using the `DMNO_CONFIG`/`DMNO_PUBLIC_CONFIG` object in your code to access your config items. This will give you full type completion and IntelliSense, and actionable error messages if you try to access a variable that doesn’t exist or fails validation.
* You can use `dmno run` to run your services with DMNO, and use `dmno dev` while you’re actively working on your schema and your apps/services will autorefresh when you make changes to your schema.
Caution
When using `process.env` you will always get back a string, even if the value is a number or boolean. This can lead to bugs that are hard to track down.
# Monorepo guide
While DMNO works great in a traditional single-purpose repo, it was designed from the ground up to handle the challenges of working in a monorepo.
## Monorepo structure
Rather than having only a single `.dmno` folder at the root of your project, in a monorepo each child project/service will have its own `.dmno` folder too.
* / (root of your project)
* .dmno (your root service config)
* **config.mts**
* packages
* my-package
* .dmno
* **config.mts** (config for my-package service)
* another-package
* .dmno
* **config.mts** (config for another-package service)
* package.json
If you’re starting fresh with DMNO, then the [`dmno init`](/docs/reference/cli/init/) command will detect all your services and help initialize DMNO in each.
## Detecting child services
By default, DMNO will rely on your existing tooling to detect where potential child projects are found. This is usually an array of paths or glob patterns.
We look in the following locations:
| Workspace tool | Globs location |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| [npm](https://docs.npmjs.com/cli/v8/using-npm/workspaces), [yarn](https://yarnpkg.com/features/workspaces#how-are-workspaces-declared), [bun](https://bun.sh/docs/install/workspaces) | `package.json` └ `workspaces` |
| [pnpm](https://pnpm.io/workspaces) | `pnpm-workspace.yaml` └ `packages` |
| [moonrepo](https://moonrepo.dev/docs/config/workspace#projects) | `.moon/workspace.yml` └ `projects` |
In some situations, like a polyglot repo, or a large repo that has multiple smaller monorepos within it, you may need an alternate way of defining where to look for DMNO services. In this case, you can create a `workspace.yaml` in your workspace root’s `.dmno` folder. If this file is found, it will override everything else.
.dmno/workpace.yaml
```yaml
projects:
- "packages/*" # glob pattern
- "libs/some-lib" # exact path
```
Not every child project needs a `.dmno` folder
These glob patterns define where to look for *potential* DMNO services, however only those services which have a `.dmno` folder will be considered DMNO services within your workspace. Shared libraries that have no need for config may not need a `.dmno` folder at all and will be ignored by DMNO.
## Running concurrent DMNO tasks
When running tasks within a monorepo, you often want to orchestrate many tasks on many child projects at once, using something like [Turborepo](https://turbo.build/repo/docs). Since many of those tasks may rely on DMNO to load and resolve config, this could slow things down due to unnecessary repeated work.
To solve this, [`dmno run`](/docs/reference/cli/run/) boots up a server that other DMNO instances within child processes are able to communicate with, meaning we can load and resolve your config just once. To take advantage of this optimization, run your command via `dmno run` and you should be good to go - for example `dmno run -- turbo build`.
Env var pass-through (Turborepo)
We use an injected `DMNO_PARENT_SERVER` env var to detect the parent server, so it must be passed through to child processes.
In [Turborepo “strict mode”](https://turbo.build/repo/docs/crafting-your-repository/using-environment-variables#strict-mode), env vars are not all passed through by default. We must explicitly tell turbo about it using the [`globalPassThroughEnv`](https://turbo.build/repo/docs/reference/configuration#globalpassthroughenv) setting. For example:
turbo.json
```json
{
"$schema": "https://turbo.build/schema.json",
"globalPassThroughEnv": [ "DMNO_PARENT_SERVER" ]
// rest of your turbo config...
```
## Sharing config across services
As outlined in our [schema guide](/docs/guides/schema/#pick), services in monorepos are allowed to `pick` items from other services. While there are other mechanisms for reusing values from other services, `pick` allows you to reuse the entire item - including all other properties (e.g., description, validation logic).
If you find your services have duplicate config items, or an item could be derived from one in another service, consider using `pick()` to keep things [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
Pick cycles are not allowed
DMNO will detect any cycles in the overall config graph, and throw errors when necessary.
# Multi-environment configuration
> Use DMNO to simplify multi-environment configuration.
DMNO supports multiple environments (e.g., dev/staging/prod) out of the box, allowing you to toggle config values based on the current environment. Many other config tools base their entire model around this concept, and you must redefine the entire set of config for each environment - resulting in copy pasted values, or awkward re-use mechanisms.
In DMNO, your config is composed of a reactive graph of data and functions. One of those built-in functions is [`switchBy`](/docs/reference/helper-methods/#switchby), which allows you to introduce branching logic based on the current value of another item. Using `switchBy` with an environment flag is the most common use-case, but there are many other useful applications as well.
## Values per environment using `switchBy`
The [`switchBy`](/docs/reference/helper-methods/#switchby) helper method selects a resolver branch based on the current value of another config item. In this case, we toggle based on an environment flag, like `NODE_ENV`, or, better yet, a custom env flag that you have more control over.
To use `switchBy`, we select another item by name to use as the switch condition, and provide a key-value object to define the different branches that might be selected. The keys are the possible expected values, with `_default` being a special reserved key that is selected if no match is found.
Note that each branch can be a static value, a function, or a resolver - just like setting the value itself. This leads to some powerful composition capabilities.
```typescript
import { switchBy } from 'dmno';
export default defineDmnoService({
schema: {
APP_ENV: {
description: 'our custom app environment flag',
extends: DmnoBaseTypes.enum(['development', 'staging', 'production', 'test']),
value: 'development',
},
TOGGLED_ITEM: {
value: switchBy('APP_ENV', {
_default: 'default/development value',
staging: 'staging value',
test: 'test value',
production: () => {
return DMNO_CONFIG.OTHER_ITEM ? 'production value' : 'production value 2';
},
}),
},
SOME_API_KEY: {
sensitive: true,
value: switchBy('APP_ENV', {
_default: devSecretsVault.item(),
production: prodSecretsVault.item(),
},
},
},
});
```
## Which environment flag should you use?
While `NODE_ENV` is a common env flag that you are probably familiar with, **we do not recommend using it**. Historically, some 3rd party modules have altered their own behaviour based on this flag, so it’s best to avoid it and leave `NODE_ENV=production`, especially in a staging environment where we want to run production-like code. It is common that other platforms inject their own env flag for the same reason.
Even in those cases, we recommend creating your own more specific env flag, for example `APP_ENV`, that you have total control over. Here is an example of building our own env flag based on the env vars injected by Vercel. In this example, we’d like to differentiate between PR previews, branch previews, and a special staging branch.
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { VercelEnvSchema } from '@dmno/vercel-platform';
export default defineDmnoService({
schema: {
...pickFromSchemaObject(VercelEnvSchema, 'VERCEL_ENV', 'VERCEL_GIT_COMMIT_REF'),
APP_ENV: {
extends: DmnoBaseTypes.enum(['development', 'branch-preview', 'pr-preview', 'staging', 'production', 'test']),
value: () => {
if (DMNO_CONFIG.VERCEL_ENV === 'production') return 'production';
if (DMNO_CONFIG.VERCEL_ENV === 'preview') {
if (DMNO_CONFIG.VERCEL_GIT_PULL_REQUEST_ID) return 'pr-preview';
else if (DMNO_CONFIG.VERCEL_GIT_COMMIT_REF === 'staging') return 'staging';
else return 'branch-preview';
}
return 'development';
},
},
SOME_VAR: switchBy('APP_ENV', { /* ... */ }),
},
});
```
In cases where the platform may not be injecting env vars, but you need to toggle the environment, you can pass it in as an env var from the command line. For example:
```bash
APP_ENV=production pnpm exec dmno run -- your-build-command
```
## Overrides from `process.env`
Keep in mind that DMNO loads in process environment variables as *overrides*, so there are many cases where your schema does not have a value defined for a certain environment flag, because you are expecting to receive it as an override. Sometimes this could be contextual information injected by the hosting platform, like `VERCEL_GIT_PULL_REQUEST_ID`, or it could be env vars that have been set via the platform’s env var management UI. This will be especially true if you are migrating an existing project to DMNO.
Over time, you may find it helpful to migrate more of your config into the schema and to a centralized secret store (like 1Password). This reduces secret sprawl, keeping things more secure and easier to reason about. In this case, you may still inject your *secret-zero* as an override, but everything else will be defined via the schema.
# Overrides
> How to load override values into your config
While we can set values in our schema, we also need to respect actual env vars coming from the current process/shell. At the very least, a single environment flag is often necessary to toggle many `switchBy` resolvers in the schema to load the correct values for a specific environment. For example: `APP_ENV=production dmno run -- your-build-command`.
Additionally, during development, we often need to temporarily toggle certain config values to test different behaviors. We want to be able to do this in a way that does not risk being accidentally checked into source control, so we must load them from somewhere other than our schema.
We refer to these values as *overrides* because they take precedence over any values set from within the schema. While env vars are the obvious example, we may also want to similarly load overrides from other sources, like [`.env` files](/docs/guides/env-files/).
DMNO’s default behavior is to treat env vars with the most precedence, and then values from `.env` files. However if you need to adjust this behavior, or load values from another source, we expose a concept of *override loaders* to do so.
Your schema must define these config items
Only config items defined in your schema will have values loaded from an override loader.
## Override loader registration
While defining a service, you may set the `overrides` property to register one or more override loaders in a specific order. These loaders will return a key-value object, and matching config items will treat these values as overrides.
.dmno/config.mts
```ts
import {
defineDmnoService, //...
processEnvOverrideLoader, dotEnvFileOverrideLoader
} from 'dmno';
import { anotherCustomOverrideLoader } from 'some-plugin';
export default defineDmnoService({
overrides: [
processEnvOverrideLoader(),
dotEnvFileOverrideLoader(),
anotherCustomOverrideLoader(),
],
//...
```
You must include the default loaders
Note that once you set any value here, you must explcitly include `processEnvOverrideLoader` and `dotEnvFileOverrideLoader` if you want to support those behaviors. This lets you explicitly adjust the precedence order as needed.
## 1Password Override Loader
While dotenv files are convenient, using them often means sensitive values sitting in plaintext on our machine. If you are using our [1Password plugin](/docs/plugins/1password/), we can use an override loader to store a dotenv style blob of overrides in a 1Password item, and have them secured behind the 1Password Desktop App’s biometric authentication 🔒🦾.
You could also use this to store a group of values shared between your team, which in some cases may be more convenient than wiring up every individual item.
Here is how we’d adjust the our config to use 2 instances of the `onePasswordOverrideLoader` accordingly:
.dmno/config.mts
```ts
import { defineDmnoService, processEnvOverrideLoader } from 'dmno';
import { OnePasswordDmnoPlugin, OnePasswordTypes, onePasswordOverrideLoader } from '@dmno/1password-plugin';
export default defineDmnoService({
name: 'api',
overrides: [
processEnvOverrideLoader(),
// personal overrides
// each dev can have a matching item in their personal "Employee" vault
onePasswordOverrideLoader(
{ reference: 'op://Employee/myapp-local-dev-overrides/api' },
{ ignoreMissing: true } // do not throw if item doesn't exist
),
// shared overrides
onePasswordOverrideLoader({ reference: 'op://non-prod-config/local-dev-env/api' }),
],
//...
```
# Schema authoring guide
> Learn how to write a full DMNO schema for your project and unlock the full power of DMNO's config management.
If you haven’t already, follow our [Quickstart](/docs/get-started/quickstart/) guide to get started with DMNO. Once you’re set up and ready to go, let’s dive in to writing a full schema for your project.
## Project structure
Using DMNO, we define a configuration schema for your entire project that lives in a `config.mts` TypeScript file within a special `.dmno` directory at the root of your project. In a monorepo project with multiple child packages/services, we keep the root config, but we also break things up into multiple `.dmno/config.mts` files that live alongside each child and provide mechanisms for them to reference each other as needed.
A typical DMNO project structure:
* Single project repo
* / (root of your project)
* **.dmno**
* .typegen/ (generated types)
* …
* .env.local (your local overrides and sensitive values)
* **config.mts** (your config schema)
* tsconfig.json (dmno specific tsconfig)
* src/
* …
* scripts/
* …
* package.json
* …etc
* Monorepo
* / (root of your project)
* **.dmno/**
* …
* packages
* api
* **.dmno/**
* …
* …
* frontend
* **.dmno/**
* …
* …
* shared-lib
* files… (no .dmno folder if no env vars are needed)
Monorepos
Check out the [monorepo guide](/docs/guides/monorepos/) for more details on using DMNO within a monorepo
### DMNO config files
Your `.dmno/config.mts` file will define settings for your project, including a full schema of all the config items used within your project. Your config file must include `export default defineDmnoService({ //...`. Here is an example:
.dmno/config.mts
```typescript
import { DmnoBaseTypes, defineDmnoService, switchBy } from 'dmno';
export default defineDmnoService({
// project settings that affect DMNO itself
settings: {
redactSensitiveLogs: true,
interceptSensitiveLeakRequests: true,
},
// configuration schema that describes all the config / env vars used in your project
schema: {
APP_ENV: {
extends: DmnoBaseTypes.enum(['development', 'staging', 'production']),
value: 'development', // default value
},
DISCORD_JOIN_LINK: {
value: 'https://chat.dmno.dev',
description: 'link for our users to join us on discord (uses a redirect)',
},
CUSTOMER_SERVICE_EMAIL: {
extends: DmnoBaseTypes.email,
description: 'The email address for customer service',
value: switchBy('APP_ENV', { // changes the value based on the value of APP_ENV
_default: 'dev@test.com',
staging: 'staging@test.com',
production: 'production@test.com',
}),
},
},
});
```
#### Multiple `.dmno/config.mts` files in a monorepo
In a monorepo, aside from the root config file, each child service will have its own `.dmno/config.mts` file, and a few more options/features become relevant:
* specifying a service `name` now becomes more important
* we can specify a `parent` service
* we can now share config items across services (see [sharing config](/docs/guides/schema/#pick) for more info)
apps/docs-site/.dmno/config.mts
```typescript
import { defineDmnoService, pick } from 'dmno';
export default defineDmnoService({
name: 'docs-site', // service name (optional - will default to `name` from `package.json`)
parent: 'frontend', // parent service name (optional - if left unset, will default to project root service)
settings: { /* add/override settings, otherwise inherited from root */ },
schema: {
// we can use `pick()` to copy config items from other services
APP_ENV: pick(), // defaults to picking from root service using the same key
DISCORD_JOIN_LINK: pick(),
CUSTOMER_SERVICE_EMAIL: pick(),
USERS_DB_URL: pick('users-db', 'DATABASE_URL'), // copies from `users-db` service and renames the key
// more config specific to this service only
DOCS_SPECIFIC_CONFIG: { value: 'foo' },
// ...
},
});
```
#### Service names
Every service must have a unique service name within your workspace. You can set it in the service’s config, although if you do not specify a name, we will use the `name` field from that service’s `package.json` file.
We recommend you set it to something short - for example `api` instead of `@my-cool-org/api` - because you may be typing it into CLI commands (e.g., `pnpm exec dmno resolve -s api`) and it will be visible in several places in terminal output.
You’ll also use it when services need to point to each other in their configuration, like when using `pick` and `parent` as seen above.
## Defining config items
Your service’s config has a `schema` which is a key-value object that describes all of the configuration your service uses. Each item has a definition that describes what kind of data it is, how to validate it, how to handle it within your build, a rich description that feeds into your IDE tooling, and in some cases, what the value is or how to generate / fetch it. More on that later.
We’ll start with a simple example from our own monorepo, and then dig into what all the options are:
```ts
export default defineDmnoService({
schema: {
// ...
GITHUB_REPO_URL: {
extends: DmnoBaseTypes.url({ allowedDomains: ['github.com'] }),
description: 'Github link to the main DMNO monorepo',
required: true,
value: () => {
return `${DMNO_CONFIG.GITHUB_ORG_URL}/${DMNO_CONFIG.GITHUB_REPO_NAME}`;
},
},
},
});
```
### Data types & `extends`
DMNO Types
DMNO has a full-featured type system that extends beyond what TypeScript is capable of on its own. When these docs refer to types we are usually talking about the DMNO type system, unless otherwise specified. See [DmnoBaseTypes](/docs/reference/base-types/) for more info.
Each item is defined by extending some base type (whether explicitly or not) and adding additional overrides on top of it.
Most of the time, you’ll use existing data types, either from [`DmnoBaseTypes`](/docs/reference/base-types/) or from a published plugin - some by DMNO, some by others. These data types are factory functions, and accept settings that control reusable behavior like validation rules and docs info. You should almost always be able to accomplish what you need with existing types - but you can author your own reusable types as well.
These types are also used to generate TypeScript types for your config and give you type safety with docs when using your config in your application code.
The syntax for `extends` is rich, and best illustrated via some examples:
```ts
export default defineDmnoService({
schema: {
// common case where a datatype is called as a function w/ settings
EXTENDS_TYPE_INITIALIZED: {
extends: DmnoBaseTypes.number({ min: 0, max: 100 }),
},
// you can skip the function call if no settings are needed
EXTENDS_TYPE_UNINITIALIZED: {
extends: DmnoBaseTypes.number,
},
// string/named format works for a few of our basic types (with no settings)
EXTENDS_STRING: {
extends: 'number',
},
// passing nothing will try to infer the type from a static value
// or fallback to a string otherwise
DEFAULTS_TO_NUMBER: { value: 42 }, // infers number
DEFAULTS_TO_STRING: { value: 'cool' }, // infers string
FALLBACK_TO_STRING_NO_INFO: { }, // assumes string
FALLBACK_TO_STRING_UNABLE_TO_INFER: { // assumes string
value: somePlugin.item(),
},
// of course you can use your own custom types (or from plugins)
USE_CUSTOM_TYPE: {
extends: MyCustomPostgresConnectionUrlType,
// additional settings can be added/overridden as normal
required: true,
},
// if no other settings are needed, you can use a shorthand and leave out the wrapping object
SHORTHAND_TYPE: MyCustomPostgresConnectionUrlType,
SHORTHAND_STRING: 'number',
},
});
```
### Sharing config between services
In a monorepo project with multiple services, DMNO allows config to be shared and composed across multiple services. In any service `schema` you can `pick` config items from other services to make them available within the service.
The `pick` function is a special kind of data type, and it copies all of the source item’s properties, not just the value. So you can think of the picked item as extending the data type of the original item. Use it like any other data type in an item’s `extends` property. There are 2 optional arguments to help specify the original item to pick from, with defaults being to pick from the root service, and use the same key/path.
```typescript
import { defineDmnoService, pick } from 'dmno';
export default defineDmnoService({
schema: {
PICK1: { extends: pick() }, // picks from [root service] > `PICK1`
PICK2: { extends: pick('other-service') }, // picks from `other-service` > `PICK2`
PICK3: { extends: pick('other-service', 'OTHER_KEY') }, // picks from `other-service` > `OTHER_KEY`
// ...
```
Using `extends` is optional
Because pick is a special kind of data type, you can use the shorthand to specify the data type only, and leave out the wrapping object with `extends`:
```ts
SHORTHAND: pick(),
// you must use the longer version if you need to add/update additional properties
SOME_ITEM: { extends: pick(), description: 'can update/add more type settings' },
```
### Validations & required config
Validating your config *BEFORE* build/run/deploy is a huge part of what makes DMNO so powerful.
You can mark items as `required: true` and they will be considered invalid if the value is empty when we load your config. Additional validations will be skipped if this is the case. Note that if the config item has a static value set, for example `value: 'some-val'` then we will infer that the item is required. In the rare case that you plan on sometimes overriding the value to `undefined`, you can add `required: false`. Note that this `required` setting affects DMNO’s generated types as to whether the value might be `undefined`.
You can also attach custom validation functions, although most of your validation needs will likely be handled by reusable base types.
```ts
export default defineDmnoService({
schema: {
VALIDATION_EXAMPLE: {
extends: DmnoBaseTypes.number({ min: 0, max: 100 }),
required: true,
validate: (val) => {
if (!isPrimeNumber(val)) throw new ValidationError('Number must be prime');
},
},
},
});
```
### Secrets & security
Items can be marked as `sensitive: true` and they will be treated accordingly. That means:
* They will NOT be exposed via `DMNO_PUBLIC_CONFIG`, only via `DMNO_CONFIG`
* We will redact their values when logging them to the console via the `dmno` CLI
* If the `redactSensitiveLogs` service setting is enabled, we will patch global `console` methods redact the value from all logs
* If the `interceptSensitiveLeakRequests` service setting is enabled, we will patch global http methods to intercept requests that send it to any domain not on the `allowedDomains` list
* Depending on the integration, we will help make sure you don’t accidentally leak them in bundled client-side javascript or server-rendered responses
To customize behavior, you can set `sensitive` to an object rather than `true`. Note that an empty object will still mark the item as being sensitive.
Resuable types may already be sensitive
Many vendor-specific resuable data types from plugins will already be marked as sensitive!
```ts
export default defineDmnoService({
schema: {
MY_SECRET: {
sensitive: true,
},
SOME_SECRET_TOKEN: {
sensitive: {
redactMode: 'show_last_2',
allowedDomains: ['api.someservice.com'],
},
},
ONE_PASS_TOKEN: {
// data types may already be marked as sensitive and have customized settings
extends: OnePasswordTypes.serviceAccountToken,
},
},
});
```
#### Redact modes
For config values that have a common prefix, showing the first 2 characters will not be very helpful for identification. We provide several different `redactMode` settings to customize how the sensitive value is displayed when redacted. The following table shows the different modes supported:
| value | description | example |
| -------------------------- | ------------------------------------------- | ---------- |
| `show_first_2` ⭐ *default* | show the **first 2** characters only | `ab▒▒▒▒▒▒` |
| `show_last_2` | show the **last 2** characters only | `▒▒▒▒▒▒yz` |
| `show_first_last` | show the **first and last** characters only | `a▒▒▒▒▒▒z` |
### Docs & IntelliSense
DMNO lets you attach additional information to items that serve as inline documentation about the item. This data is also used to generate TypeScript JSDoc comments for your config - giving you and your team ✨ magical IDE superpowers.
Tip
Some of these settings make more sense within the context of authoring reusable types, but they are all always available.
```ts
export default defineDmnoService({
schema: {
INTELLISENSE_DEMO: {
required: true,
sensitive: true,
summary: 'Primary DB URL',
description: 'houses all of our users, products, and orders data',
// description of the type of the data rather than this instance of it
typeDescription: 'Postgres connection url',
externalDocs: {
description: 'explanation (from prisma docs)',
url: 'https://www.prisma.io/dataguide/postgresql/short-guides/connection-uris#a-quick-overview',
},
ui: {
// uses iconify names, see https://icones.js.org for options
icon: 'akar-icons:postgresql-fill',
color: '336791', // postgres brand color :)
},
},
},
});
```
An example of how the generated types show up with VSCode’s IntelliSense: 
### Dynamic vs static
Items can use `dynamic: true` or `false` to override their behaviour as to whether they should be bundled into your code at *build* time versus always loaded at *boot* time. This is only relevant for some integrations/projects, and it’s a *big* topic. See our [dynamic config guide](/docs/guides/dynamic-config/) for more details.
### Setting item values
While some tools may let you set only default values for config items, DMNO lets you set the value from within your schema for all situations.
This is possible because the `dmno` config loading process is broken up into 2 stages: first we load the schema, and then we resolve the values. While the resolution process does respect overrides passed in as process environment variables and other sources like .env files, many of your values may be set *from the schema directly*.
We can use static values, inline functions, or a *resolver* - which is basically just a fancy function that will be called during the resolution process and passed some contextual data about the config item and the rest of the resolved config.
An example of setting the `value` to each of these cases in our schema:
```ts
export default defineDmnoService({
schema: {
// static value (for constants or defaults planned to be overridden)
STATIC_VAL: {
value: 'static',
},
// use an inline function which references other item values
INLINE_FN_VAL: {
value: () => `prefix_${DMNO_CONFIG.OTHER_ITEM}`,
},
// using an instance of a "resolver" from a plugin
RESOLVER_EXAMPLE: {
value: somePlugin.fetchSecretItemById('xyz'),
},
},
});
```
Internally, we even wrap the static values and inline functions into resolvers, so that we can always display some additional metadata about *how* the value will be resolved - even before attempting to perform the resolution. There is also a concept of branching to handle things like if-else and switch statements that point to more resolvers, leading to some very powerful composition capabilities.
A quick example to illustrate using our built-in [`switchBy`](/docs/reference/helper-methods/#switchby) resolver which switches between several branches based on the current value of another within your config.
```ts
export default defineDmnoService({
schema: {
APP_ENV: {
extends: DmnoBaseTypes.enum(['development', 'staging', 'production', 'test']),
description: 'our custom environment flag',
},
SOME_API_KEY: {
sensitive: true,
value: switchBy('APP_ENV', {
// static values can be used if the value is not actually sensitive
_default: 'dev123',
test: 'test123',
// sensitive keys we need to pull from somewhere secure using plugins
staging: devSecretsVault.item(),
production: prodSecretsVault.item(),
}),
},
},
});
```
You can author your own reusable resolvers - but you likely won’t need to for most use cases.
# Secret segmentation
> Learn how to use DMNO's plugin instances to manage secrets for different environments or services.
There are many ways to segment secrets in DMNO. The most common is via [plugin instances](/docs/plugins/overview/#multiple-plugin-instances). This allows you to create a separate instance for each environment or service, giving you full control over which secrets are used where.
In general, you should apply the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) when it comes to secrets. The facets of this are different for each organization, but as a rule of thumb, you should aim to:
* Minimize the number of people who have access to secrets, and regularly review and audit who does
* Minimize the environments (e.g., dev, staging, production) and their associated machine identities that have access to secrets
* Rotate secrets whenever possible
* Use short-lived credentials whenever possible
## Sample setups
Assuming something resembling a trunk-based workflow (i.e., you have a single production environment and are using feature flags to manage changes across environments), the minimum viable setup that we recommend looks something like this:
* a plugin instance for dev secrets - those that are necessary for local development and any shared dev services
* a plugin instance for prod secrets - those that are necessary for production services
The next step would be split out those plugin instances per service in each environment. For example, say your application has a `web` service and a `backend` service. You would then create `web-dev`,`web-prod`, `backend-dev`, and `backend-prod` plugin instances.
We’re not advocating for these as optimal setups for all applications, but they’re a good starting point. The more you can apply the principle of least privilege, the better.
# TypeScript configuration
> Configuring TypeScript
Other tools try to *infer* TypeScript types directly from a configuration schema. While it’s impressive how far this approach has come since the early days, the types used during inference are extremely complex and there are always going to be limitations. Plus importing these config tools directly into your code can introduce headaches with ESM/commonjs and tsconfig files.
Instead, while DMNO still uses TypeScript to define your schema in your `.dmno/config.mts` files, it is **decoupled from your code**. We run our own build process using [Vite](https://vitejs.dev) that *just works* without any configuration problems, and we generate `.d.ts` files with built-in documentation (JSDoc comments) to be consumed by your code. Our type system is also much more powerful, with an inheritance mechanism not possible in other systems.
*Example of VSCode’s IntelliSense using DMNO’s generated types:*

Polyglot types coming soon
Decoupling the schema from the consumed types means it will be easier to generate types to be used in other languages!
### Accessing the types in your code
To simplify accessing your config, we inject the `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG` globals. We must let TypeScript know about them in order to allow type-checking when *using* your config and to get autocompletion/IntelliSense in your IDE.
The DMNO Config loader automaticaly generates TS types to be consumed by your code into the `.dmno/.typegen` directory:
* `.dmno/.typegen/global.d.ts` - injects the `DMNO_CONFIG` global
* `.dmno/.typegen/global-public.d.ts` - injects the `DMNO_PUBLIC_CONFIG` global
* `.dmno/schema.d.ts` - the actual schema of your config, used by the `.d.ts` files
*We use these same types to give you autocompletion when authoring your schema itself.*
The easiest way to let TypeScript know about them is to create a `dmno-env.d.ts` file in your source code that imports them via [triple-slash](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) references.
FYI
Running `dmno init` will do this for you, creating the file either at the root of your service or in a `src` directory if you have one.
In many cases this will be enough and TypeScript will already pick up the new types.
src/dmno-env.d.ts
```ts
// inject DMNO_CONFIG global
///
// inject DMNO_PUBLIC_CONFIG global
///
```
However in some rare cases - like where you have some set of isolated source files split from the rest of your code, you may need to be a bit more explicit. You can do this by adding references to the generated type files either in your tsconfig or in a specific source file directly.
For example, in your tsconfig:
tsconfig.node.json
```jsonc
{
// ...
"include": [
"vite.config.ts",
".dmno/.typegen/global.d.ts",
".dmno/.typegen/global-public.d.ts",
]
}
```
Or in a specific file:
vite.config.ts
```ts
///
///
import { defineConfig } from 'vite';
// ...
```
Caution
If TypeScript doesn’t somehow know about the `DMNO_CONFIG` globals, then while things may still work, you’ll be missing out valuable help from your IDE.
#### Pure JS projects
Even if you are not writing TypeScript, these days you are still likely relying on TypeScript for autocompletion in your IDE. If you don’t use a [`jsconfig.json` file](https://code.visualstudio.com/docs/languages/jsconfig), your editor will likely just pick up the `dmno-env.d.ts` file automatically. But if you do have one, you might need to explicitly add it to your `include` globs. For example:
jsconfig.json
```json
{
// ...
"include": [
"**/*.js",
".dmno/.typegen/global.d.ts",
".dmno/.typegen/global-public.d.ts"
]
}
```
Invalid keys are not strict
JavaScript is by default not strict - so while you will get nice autocompletion and IntelliSense on your config, your IDE will not give you the handy red squiggle if you use an *invalid* key. Depending on your setup you may still get a build/runtime error.
#### Injecting `DMNO_CONFIG` vs `DMNO_PUBLIC_CONFIG`
To keep things simple, by default we always inject the global types for both `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG`. However, there are cases where you may want to only inject one or the other:
* In a front-end only (i.e., non-SSR) context, you could skip injecting `DMNO_CONFIG` because you’ll only want to use non-sensitive config items. That said, injecting the *types* doesn’t actually inject your sensitive secrets, and we inject a placeholder proxy throws a helpful error if you try to use `DMNO_CONFIG`.
* In a back-end only context, you could skip injecting `DMNO_PUBLIC_CONFIG` and exclusively use `DMNO_CONFIG` - just like we do in your `.dmno/config.mts` file. However, there isn’t really any harm in using the public version, and it can serve as an extra reassurance that something is definitely not sensitive - which is useful if you were, for example, returning a config item in an API response.
# Astro Integration
> Use DMNO to manage your Astro app's environment variables for static, hybrid, and server-side rendering.
At DMNO we *love* [Astro](https://astro.build/). This very site is built with it! That’s why we’re very excited to make it easier and safer to use environment variables in all of your Astro-powered projects, whether it’s on the client or the server.
## Setup
### Initialize your Astro integration
Using [`dmno init`](/docs/reference/cli/init/) will automatically detect that you are using Astro and install the necessary packages and configuration for you.
* npm
```bash
npx dmno init
```
* pnpm
```bash
pnpm dlx dmno init
```
* Yarn
```bash
yarn dlx dmno init
```
* Bun
```bash
bunx dmno init
```
> Skip to [Configure…](#configure-your-environment-variables) once this is complete.
Note
If you run into any issues, feel free to [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=integrations%2Fastro%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A) and try the manual installation steps below.
#### Manual install instructions
If you prefer, you can install the `dmno` and `@dmno/astro-integration` packages manually:
* npm
```bash
npm add @dmno/astro-integration dmno
```
* pnpm
```bash
pnpm add @dmno/astro-integration dmno
```
* Yarn
```bash
yarn add @dmno/astro-integration dmno
```
* Bun
```bash
bun add @dmno/astro-integration dmno
```
Update your `astro.config.mjs` - import the plugin, and add to `defineConfig`:
astro.config.mjs
```js
import { defineConfig } from 'astro/config';
import dmnoAstroIntegration from '@dmno/astro-integration';
export default defineConfig({
// ...
integrations: [dmnoAstroIntegration()],
});
```
### Configure your environment variables
`dmno init` will scaffold out the `schema` in your `config.mts` files based on any existing `.env` files and references to `process.env`/`import.meta.env` found within your codebase. See our [Schema Guide](/docs/guides/schema/) for the specifics of how to author additional updates to your DMNO `schema`.
## Accessing config
DMNO globally injects your config into your application. You can access it via:
* `DMNO_CONFIG` - includes *all* of your config items
* `DMNO_PUBLIC_CONFIG` - includes only items not marked with `sensitive: true`
You can now access these with full type-safety and autocompletion just about everywhere in your code - including astro components, vue/react/etc, mdx files, even your `astro.config.*`!
Why globals?
It’s really no different than using `process.env` or `import.meta.env` - but by using our own variables, we can provide a more consistent experience and sprinkle some extra magic on top ✨
### Protecting secrets from leaking
In theory, you should only access `DMNO_PUBLIC_CONFIG` on the *client*, and you can access both `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG` on the server.
Sounds easy, right? Except in a world of hybrid client/server rendering and rehydration, and when you may actually need sensitive config during a server-side render, it can be hard to keep track of what is getting rendered where.
So, we make it easy for you:
* Within the browser/client, you only have access to `DMNO_PUBLIC_CONFIG` and if you try to access `DMNO_CONFIG`, we’ll throw a helpful error 🛑
* We detect leaked secrets in built JS code and server-rendered responses, just in case you leaked a secret into ANYTHING getting sent over the wire (opt-in via `preventClientLeaks` service setting)
* We redact sensitive data from logs (opt-in via `redactSensitiveLogs` service setting)
* We intercept HTTP requests if you send sensitive config somewhere it’s not supposed to go (opt-in via `interceptSensitiveLeakRequests` service setting)
 *an example of our middleware in action*
Check out the [security guide](/docs/get-started/security/) for more details on our opt-in security features.
### Static vs dynamic config
DMNO gives you explicit control over how your config items are treated - whether they will be replaced into your bundled code at build time (i.e., static), or reloaded at boot time (i.e., dynamic). See the [dynamic config guide](/docs/guides/dynamic-config/) for more details.
Client-side loading of dynamic config is automatically enabled if you are not using `output: 'static'` mode and you have non-sensitive dynamic config items in your schema. It will be fetched on-demand, so if you don’t use those items on the client, that’s also fine. This integration will automatically inject the API route required to fetch public+dynamic config.
Additionally, this integration throws an error during `astro build` if you try to use a dynamic config item during a pre-render of any static page/endpoint - regardless of the [output mode](https://docs.astro.build/en/basics/rendering-modes/#server-output-modes) you are using.
No matter what, dealing with config in a hybrid server/client rendering setup is confusing and full of footguns, so we do our best to protect you 🛡️ and make it as easy as possible.
***
## Common recipes
### Using env vars within `astro.config.*`
It’s often useful to be able to access configuration / env vars within your Astro config. Without DMNO, it’s a bit awkward, but DMNO makes it dead simple - in fact it’s already available! Just reference config vars via `DMNO_CONFIG.SOME_ITEM` like you do everywhere else.
In most Astro projects, it should just work, but if you are seeing type errors about `DMNO_CONFIG` not existing, you can add a triple slash reference to the generated types. For example:
astro.config.ts
```ts
///
import { defineConfig } from 'astro/config';
// ...
```
see our [TypeScript guide](/docs/guides/typescript/) for more details.
### Injecting config into markdown files
Markdown files are processed by Astro but treated as pure content without evaluating any javsascript. So if you need to inject any DMNO\_CONFIG values, you’ll need to use MDX instead.
See [@astrojs/mdx integration](https://docs.astro.build/en/guides/integrations-guide/mdx/)
Then you need to use JSX within your markdown content. For example:
```mdx
# Header with config item { DMNO_PUBLIC_CONFIG.SOME_VAR }
```
For links, you’ll need to use the html/jsx version rather than a markdown style link:
```mdx
Join us on our Discord
Join us on our [Discord](DMNO_PUBLIC_CONFIG.DISCORD_JOIN_URL)
Join us on our [Discord]({DMNO_PUBLIC_CONFIG.DISCORD_JOIN_URL})
Join us on our [Discord]({ DMNO_PUBLIC_CONFIG.DISCORD_JOIN_URL })
```
### Injecting config into inline script tags
Another case where Astro may not process the code you write and inject DMNO\_CONFIG is within the body of an inline script. Attributes do work though, so here is one workaround you can use:
**GoogleAnalytics.astro**
```jsx
```
# Fastify
> Use DMNO to manage your Fastify app's environment variables
Fastify does not provide any built-in handling of env vars, although there is a [fastify-env](https://github.com/fastify/fastify-env) plugin, which internally uses dotenv files with json schema and ajv for validations. Whether you use that plugin, or have wired up something else yourself, we think using DMNO instead is worth the additional benefits.
Compatibility - TypeScript + ESM
This plugin works if you are using JavaScript or TypeScript, but currently it only supports ESM projects.
If you have a CommonJS Fastify app and you’d like to use DMNO, please contact us on [our Discord](https://chat.dmno.dev)!
### Installation
While `dmno init` will automatically detect that you are using Fastify and install the necessary packages for you, you may also want to install them yourself:
* npm
```bash
npm add @dmno/fastify-integration dmno
```
* pnpm
```bash
pnpm add @dmno/fastify-integration dmno
```
* Yarn
```bash
yarn add @dmno/fastify-integration dmno
```
* Bun
```bash
bun add @dmno/fastify-integration dmno
```
The rest of the Fastify setup looks slightly different depending on if you areusing the [fastify-cli](https://github.com/fastify/fastify-cli) or not. Select your situation using the tabs below:
#### Initialize + register `dmnoFastifyPlugin`
* Direct usage
Wherever you initialize your `fastify` instance and register plugins, import and register our `dmnoFastifyPlugin`:
```js
import Fastify from 'fastify';
import { dmnoFastifyPlugin } from '@dmno/fastify-integration';
const fastify = Fastify({ /* ... */ });
fastify.register(dmnoFastifyPlugin);
fastify.register(someOtherPlugin);
```
* fastify-cli
Create a new file in the plugins directory to initialize the plugin:
src/plugins/dmno.js
```js
import { dmnoFastifyPlugin } from "@dmno/fastify-integration";
export default dmnoFastifyPlugin;
```
#### Adjust your package.json scripts
In this case, we must run our `dev` and `start` commands via [`dmno run`](/docs/reference/cli/run/). You’ll want to adjust your `package.json` scripts accordingly. Your existing scripts may not match exactly, but that’s ok. Just note that if you want live reload you need to include the `-w` flag.
* Direct usage
package.json
```json
{
"name": "yourapp",
"scripts": {
"dev": "dmno run -w -- nodemon src/main.js",
"start": "dmno run -- node src/main.js"
},
//...
```
* fastify-cli
pacakge.json
```json
{
"name": "your-fastify-cli-app",
"scripts": {
"start": "npm run build:ts && dmno run -- fastify start -l info dist/app.js",
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
"dev:start": "dmno run -w -- fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js"
//...
}
//...
```
One-off scripts
You’ll probably also want to run any other one-off scripts or other tools that require config via `dmno run` so that they get your config and secrets injected as well.
### Configure your configuration schema
`dmno init` will scaffold out the `schema` in your `config.mts` files based on your existing `.env` files. See our [Schema Guide](/docs/guides/schema/) for the specifics of how to author additional updates to your DMNO `schema`.
## Accessing config
> Use `DMNO_CONFIG` instead of `process.env` 🎉
You’ll now have fully typed and validated config and some cool security features described below.
## Security and leak prevention
Aside from the general DX improvements that DMNO provides, it also introduces important security features to keep your secrets safe:
* redacts your sensitive config from logs
* intercepts requests that send sensitive config to hosts not on an allow list
* stops returning sensitive config as part of server responses
You can read more about these features and how to enable/disable them in our [Security Guide](/docs/get-started/security/).
The Fastify plugin does its best to enable these things automatically, but it would be entirely reasonable to disable these features, and use the underlying helpers to customize the behavior.
# Next.js
> Use DMNO to manage your Next.js app's environment variables for static, hybrid, and server-side rendering.
Turbopack not supported
This integration is currently not compatible with Turbopack. You will need to remove the `--turbopack` flag from your `next dev` command when using `dmno run`. We’re working on a solution for this, and will update this page when it’s ready.
If you have an existing Next.js app, you’re probably already familiar with how environment variables work in Next. Check their docs [here](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables) if you need a refresher.
Now forget all of that, and let’s simplify things with DMNO. 🥳
### Initialize your Next.js integration
Using `dmno init` we will automatically detect that you are using Next.js and install the necessary packages and configuration for you.
* npm
```bash
npx dmno init
```
* pnpm
```bash
pnpm dlx dmno init
```
* Yarn
```bash
yarn dlx dmno init
```
* Bun
```bash
bunx dmno init
```
> Skip to [Configure…](#configure-your-environment-variables) once this is complete.
## Manual Setup
If you prefer, you can install `dmno` itself and the `@dmno/nextjs-integration` package manually:
* npm
```bash
npm add @dmno/nextjs-integration dmno
```
* pnpm
```bash
pnpm add @dmno/nextjs-integration dmno
```
* Yarn
```bash
yarn add @dmno/nextjs-integration dmno
```
* Bun
```bash
bun add @dmno/nextjs-integration dmno
```
Then, in your `next.config.mjs` file, import and initialize our `dmnoNextConfigPlugin`:
```js
import { dmnoNextConfigPlugin } from '@dmno/nextjs-integration';
/** @type {import('next').NextConfig} */
const nextConfig = {
// your existing config...
};
export default nextConfig;
export default dmnoNextConfigPlugin()(nextConfig);
```
### Configure your environment variables
`dmno init` will scaffold out the `schema` in your `config.mts` files based on your existing `.env` files. See our [Schema Guide](/docs/guides/schema/) for the specifics of how to author additional updates to your DMNO `schema`.
### Adjusting package.json scripts
Unlike some of our other integrations, this integration requires that you run your `next` commands via [`dmno run`](/docs/reference/cli/run/). The `dmno init` setup helper will try to do this for you, but you’ll want to make sure your package.json scripts look something like this:
package.json
```json
{
"name": "yourapp",
"scripts": {
"dev": "dmno run -w -- next dev",
"build": "dmno run -- next build",
"start": "dmno run -- next start",
"lint": "dmno run -- next lint"
},
// ...
}
```
Tip
Scripts defined in package.json will resolve commands automatically from your installed `node_modules` directory, so using `dmno run -- othercommand` works.
But if you are running a command manually, `dmno` will not usually be in your `PATH` and so your terminal will not know what to do. Typically you can use your package manager to find the `dmno` executable, for example: `pnpm exec dmno run -- othercommand`, `npm exec` or `yarn exec`.
## Accessing config
> Use `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG` instead of `process.env` 🎉
You’ll now have fully typed and validated config, fine grained control over static/dynamic behaviour, and some cool security features described below.
### Security, secrets, and leak detection
Only `DMNO_PUBLIC_CONFIG` is available in code running on the client. That said, since Next.js does so much magic under the hood, it can be difficult to reason about whether the code you are writing will run on the server, client, or both. This makes it difficult to be 100% certain that your sensitive config will not be leaked.
To protect you from this risk, DMNO does has several security related features:
* **Leak detection** - built client-side code and server-rendered responses are scanned for any sensitive config items
* **Log redaction** - sensitive config values are redacted from `console.log` output and other console methods
* **HTTP request interception** - http requests are intercepted and stopped if sending sensitive config to the disallowed domains
These features are opt-in - check out the [security guide](/docs/get-started/security/) for more details.
Note
In general, these features should *just work* but the matrix of app/pages router, node/edge, pages/api, and hosting platforms means that things are quite complicated. If you notice any issues, please [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=integrations%2Fnextjs%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A)!
### Dynamic public config
If you’d like to be able to alter certain configuration values at boot time and load them in the client rather than relying on values bundled into your code, you need to expose an API endpoint which exposes this **public+dynamic** config.
See the [dynamic config guide](/docs/guides/dynamic-config/) for more details.
Unfortunately, NextJS does not let us automatically inject the API route required to expose these config values, so if you want to use this feature, you must manually add an api route. This will be slightly different depending on if you are using app or pages router. Note the file paths in the examples below, they must match exactly.
* App Router
app/api/fetch-dynamic-public-config/route.ts
```ts
export const dynamic = 'force-dynamic';
export async function GET() {
return Response.json((globalThis as any)._DMNO_PUBLIC_DYNAMIC_OBJ);
}
```
* Pages Router
pages/api/fetch-dynamic-public-config.ts
```ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json((globalThis as any)._DMNO_PUBLIC_DYNAMIC_OBJ)
}
```
NOTE - fetching this config makes a **blocking** http request, so you should think carefully about if and how you use this feature, especially if performance is important your site. See the [dynamic config guide](/docs/guides/dynamic-config/) for more details.
# Node.js (Express, Koa, etc)
> More effectively manage your Node.js configuration with DMNO
While Node.js recently added [native support for loading dotenv files](https://nodejs.org/docs/latest/api/cli.html#--env-fileconfig), you’re often still left hacking things together to get a config system that works for your needs. DMNO supports Node out-of-the-box, with no additional plugins to install.
## Setup
To get started, install `dmno` and then set up your config schema according to the [schema guide](/docs/guides/schema/). **No additional integration-specific package is needed!**
* npm
```bash
npm add dmno
```
* pnpm
```bash
pnpm add dmno
```
* Yarn
```bash
yarn add dmno
```
* Bun
```bash
bun add dmno
```
### Loading your config
You must trigger the loading of the `DMNO_CONFIG` global, ideally as the *first thing* you do in your application code. To do this, simply `import 'dmno/auto-inject-globals'` at the top of any entrypoint into your code, typically something like `app.ts`/`main.ts`. Do the same in any other script files you may need to run.
main.ts
```ts
import 'dmno/auto-inject-globals'; // should be imported first!
// rest of your imports and code...
```
### Booting your app
When using dmno directly with node, rather than via a framwork-specific integration, you must adjust the command(s) you use to run your code by using `dmno run`. This resolves your config values and injects them into your running process. For example:
package.json
```json
{
// ...
"scripts": {
"start": "dmno run -- node dist/main.js",
},
```
## Accessing config values
Use `DMNO_CONFIG.SOME_ITEM` within your code and you are good to go!
```ts
const someApiClient = new SomeApiClient(process.env.SOME_API_SECRET);
const someApiClient = new SomeApiClient(DMNO_CONFIG.SOME_API_SECRET);
```
### External tools
Other tools likely need access to some of your config as well - for example a database migration tool might need the full connection string/url for your database.
In some cases you may have a custom wrapper script, in which case, `import 'dmno/auto-inject-globals'` will do the trick. In other cases, you may want to call those tools directly via their executables - whether directly on the command line or via package.json scripts.
In this case, run the command via `dmno run` and your config will be loaded and passed back into `process.env` which is usually where those external tools will be looking. It’s important to note that when we pass the config back into `process.env`, **we convert everything back into strings**, because this matches how normal env vars passed into process.env work. This should not be a problem since those tools were likely expecting regular environment variables (i.e., strings) in the first place, but it is important to remember.
Tip
Scripts defined in package.json will resolve commands automatically from your installed `node_modules` directory, so using `dmno run -- othercommand` works.
But if you are running a command manually, `dmno` will not usually be in your PATH and so your terminal will not know what to do. Typically you can use your package manager to find the `dmno` executable, for example: `pnpm exec dmno run -- othercommand`, `npm exec`, or `yarn exec`.
package.json
```json
{
// ...
"scripts": {
"migrate": "prisma migrate dev",
"migrate": "dmno run -- prisma migrate dev",
},
```
## Recipes
### Watch mode and dev commands
You likely have a `pnpm dev`, or `yarn`/`npm`/etc, command - which you rely on for automatically reloading as you make changes to your *application code*. It might use [nodemon](https://nodemon.io/), Node.js’ [native `--watch` option](https://nodejs.org/docs/latest/api/cli.html#--watch), or something else. Similarly, `dmno run` has a “watch mode” (`-w`) which watches your *config* and reloads when any changes are detected.
By running your watch/dev tool via `dmno run`, you’ll get your app automatically reloading whenever you make changes to your config or application code - all while validating your config, regenerating types, etc.
Prefix your existing dev command with `dmno run -w --` and you’re good to go.
package.json
```json
{
// ...
"scripts": {
"dev:nodemon": "dmno run -w -- nodemon dist/main.js",
"dev:native": "dmno run -w -- node --watch --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm ./src/main.ts",
},
```
### Scanning for leaked secrets
If you are building a Node.js API, you may want to scan for leaked secrets.
We will be releasing middlewares for popular frameworks (e.g., express, koa, fastify) very soon!
# Integrations overview
> Use DMNO with your favorite frameworks and tools including: Astro, Next.js, Vite, and Node.js.
With DMNO you get:
* a single system to manage your config for all of your services
* secure handling of your secrets by committing them encrypted to your repo or sync with backends like 1Password
* validations, coercion logic, and full type safety
* built in documentation, and magical IDE auto-completion
* an extremely flexible type system
* live reload and re-validation on config changes
## Integrations
Legend:
* ✅ Supported
* 🧰 Partially Supported
* 🚧 In Progress
* 🗓️ Planned
* ❌ Not Supported
### [Remix](/docs/integrations/remix/)
| Feature | Description | Supported | Notes |
| :--------------- | :----------------------------- | :-------- | :---- |
| Public Config | Access non-sensitive items | ✅ | |
| Sensitive Config | Access sensitive items | ✅ | |
| Dynamic Config | Access config items at runtime | ✅ | |
| Middleware | Detects leaked secrets | ✅ | |
### [Astro](/docs/integrations/astro/)
| Feature | Description | Supported | Notes |
| :--------------- | :----------------------------- | :-------- | :---- |
| Public Config | Access non-sensitive items | ✅ | |
| Sensitive Config | Access sensitive items | ✅ | |
| Dynamic Config | Access config items at runtime | 🧰 | |
| Middleware | Detects leaked secrets | ✅ | |
### [Next.js](/docs/integrations/nextjs/)
| Feature | Description | Supported | Notes |
| :--------------- | :----------------------------- | :-------- | :---- |
| Public Config | Access non-sensitive items | ✅ | |
| Sensitive Config | Access sensitive items | ✅ | |
| Dynamic Config | Access config items at runtime | 🧰 | |
| Middleware | Detects leaked secrets | 🧰 | |
### [Vite](/docs/integrations/vite/)
| Feature | Description | Supported | Notes |
| :--------------- | :--------------------------------- | :-------- | :---- |
| Public Config | Access non-sensitive items | ✅ | |
| Sensitive Config | Access sensitive items | ✅ | |
| Dynamic Config | Access config items at runtime | 🧰 | |
| DMNO Types | Framework specific types from DMNO | 🗓️ | |
| Middleware | Detects leaked secrets | 🗓️ | |
### [Fastify](/docs/integrations/fastify/)
| Feature | Description | Supported | Notes |
| :--------------- | :------------------------- | :-------- | :---- |
| Public Config | Access non-sensitive items | ✅ | |
| Sensitive Config | Access sensitive items | ✅ | |
| Middleware | Detects leaked secrets | ✅ | |
# Remix
> Use DMNO to manage your Remix app's environment variables for static, hybrid, and server-side rendering.
Remix doesn’t provide any env var tooling itself, but their [docs do mention a few tips](https://remix.run/docs/en/main/guides/envvars). Using DMNO, we make managing your configuration in Remix apps even simpler, and provide a ton of additional features including:
* type safety + validation
* leak detection and prevention
* full control over server/client bundling behaviour
* built-in support for fetching dynamic config items on the client
### Initialize your Remix integration
Using `dmno init` we will automatically detect that you are using Remix and install the necessary packages and configuration for you.
* npm
```bash
npx dmno init
```
* pnpm
```bash
pnpm dlx dmno init
```
* Yarn
```bash
yarn dlx dmno init
```
* Bun
```bash
bunx dmno init
```
> Skip to [Configure…](#configure-your-environment-variables) once this is complete.
## Manual Setup
If you prefer, you can install `dmno` itself and the `@dmno/remix-integration` package manually:
* npm
```bash
npm add @dmno/remix-integration dmno
```
* pnpm
```bash
pnpm add @dmno/remix-integration dmno
```
* Yarn
```bash
yarn add @dmno/remix-integration dmno
```
* Bun
```bash
bun add @dmno/remix-integration dmno
```
Then, in your `vite.config.ts` file, import and initialize our `dmnoRemixVitePlugin` and `dmnoRemixPreset`:
```js
import { dmnoRemixVitePlugin, dmnoRemixPreset } from "@dmno/remix-integration";
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
dmnoRemixVitePlugin(),
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
},
presets: [dmnoRemixPreset()],
}),
tsconfigPaths(),
],
});
```
### Configure your environment variables
`dmno init` will scaffold out the `schema` in your `config.mts` files based on your existing `.env` files. See our [Schema Guide](/docs/guides/schema/) for the specifics of how to author additional updates to your DMNO `schema`.
## Accessing config
> Use `DMNO_CONFIG` and `DMNO_PUBLIC_CONFIG` instead of `process.env` 🎉
You’ll now have fully typed and validated config, fine grained control over static/dynamic behaviour, and some cool security features described below.
### Security, secrets, and leak detection
Only `DMNO_PUBLIC_CONFIG` is available in code running on the client. That said, since Remix does so much magic under the hood, it can be difficult to reason about whether the code you are writing will run on the server, client, or both. This makes it difficult to be 100% certain that your sensitive config will not be leaked.
To protect you from this risk, DMNO does has several security related features:
* **Leak detection** - built client-side code and server-rendered responses are scanned for any sensitive config items
* **Log redaction** - sensitive config values are redacted from `console.log` output and other console methods
* **HTTP request interception** - http requests are intercepted and stopped if sending sensitive config to the disallowed domains
These features are opt-in - check out the [security guide](/docs/get-started/security/) for more details.
Note
In general, these features should *just work* but if you notice any issues, please [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=integrations%2Fremix%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A)!
### Dynamic public config
If you’d like to be able to alter certain configuration values at boot time and load them in the client rather than relying on values bundled into your code, you need to expose an API endpoint which exposes this **public+dynamic** config.
See the [dynamic config guide](/docs/guides/dynamic-config/) for more details.
NOTE - fetching this config makes a **blocking** http request, so you should think carefully about if and how you use this feature, especially if performance is important your site. See the [dynamic config guide](/docs/guides/dynamic-config/) for more details.
# Vite
> Use DMNO to add type safety and validation to your Vite project's configuration.
At DMNO we’re big fans of [Vite](https://vitejs.dev/), and we use it under the hood to parse your `.dmno/config.mts` files. But our internal instance of Vite is decoupled from yours, so this plugins provides first class support between your use of Vite and DMNO.
This plugin enables:
* automatic loading of dmno resolved config (no `dmno run` needed)
* build-time replacement of [static](/docs/guides/dynamic-config/) config items in your code and [html template](https://vite.dev/guide/env-and-mode#html-env-replacement)
* ability to use config items within `vite.config.*` file
* automatic restart of the Vite server on config changes
* config validation during development and build with helpful error messages
## Initialize your Vite integration
Using `dmno init` we will automatically detect that you are using Vite and install the necessary packages and configuration for you.
* npm
```bash
npx dmno init
```
* pnpm
```bash
pnpm dlx dmno init
```
* Yarn
```bash
yarn dlx dmno init
```
* Bun
```bash
bunx dmno init
```
This will create a `.dmno` directory in the root of your project with a `config.mts` file.
> Skip to [Configure…](#configure-your-environment-variables) once this is complete.
## Manual Setup
If you prefer, you can install `dmno` itself and the `vite-integration` package manually:
* npm
```bash
npm add @dmno/vite-integration dmno
```
* pnpm
```bash
pnpm add @dmno/vite-integration dmno
```
* Yarn
```bash
yarn add @dmno/vite-integration dmno
```
* Bun
```bash
bun add @dmno/vite-integration dmno
```
### Configure the dmno Vite plugin
Update your `vite.config.ts` - import the plugin, and add to `defineConfig`:
vite.config.ts
```ts
import { injectDmnoConfigVitePlugin } from '@dmno/vite-integration';
export default {
// ...
plugins: [injectDmnoConfigVitePlugin()],
};
```
Ordering
You should import the `@dmno/vite-integration` first because it loads your config and re-injects it into process.env, just in case any other plugins are looking for something there.
The order of `injectDmnoConfigVitePlugin()` in the plugins does not matter.
### Configure your environment variables
`dmno init` will scaffold out the `schema` in your `config.mts` files based on your existing `.env` files. See our [Schema Guide](/docs/guides/schema/) for the specifics of how to author additional updates to your DMNO `schema`.
## Accessing config
Most vanilla Vite setups are for building static front-end apps. Therefore we are mostly concerned with injecting non-sensitive static config into our built code. Use `DMNO_PUBLIC_CONFIG` instead of `process.env` or `import.meta.env`, and you’ll get all the benefits of dmno, and no longer have to rely on special `PUBLIC_` prefixes. By default, **only static items referenced via `DMNO_PUBLIC_CONFIG` items will be replaced**.
src/some-file.ts
```ts
if (DMNO_PUBLIC_CONFIG.SERVICE_X_ENABLED) {
const client = new ServiceXClient(DMNO_PUBLIC_CONFIG.SERVICE_X_PUBLIC_KEY);
}
```
If you are building for a server/hybrid environment, you can toggle on the `injectSensitiveConfig` option to also replace static items accessed via `DMNO_CONFIG`, which will include sensitive items as well.
vite.config.ts
```ts
import { defineConfig } from 'vite'
import { injectDmnoConfigVitePlugin } from '@dmno/vite-integration';
export default defineConfig({
plugins: [
injectDmnoConfigVitePlugin({ injectSensitiveConfig: true })
],
//...
```
Static config replacement
Only static items will be replaced at build time. The default handling is controlled by a service-level `dynamicConfig` setting, and can be overridden using the `dynamic` property on each item. See the [dynamic config guide](/docs/guides/dynamic-config/) for more info.
### Using env vars within `vite.config.*`
It’s often useful to be able to access env vars in your Vite config. Without DMNO, it’s a bit awkward, but DMNO makes it dead simple - in fact it’s already available! Just reference config vars via `DMNO_CONFIG.SOME_ITEM` like you do everywhere else.
In many Vite projects, your `vite.config.*` file is not included in the same `tsconfig` as the rest of your code. If this is the case, and you are seeing type errors about `DMNO_CONFIG` not existing, you can add a triple slash reference to the generated types. For example:
vite.config.ts
```ts
///
import { defineConfig } from 'vite';
// ...
```
See our [TypeScript guide](/docs/guides/typescript/) for more details.
### Using config within other scripts
Even in a static front-end project, you may have other scripts in your project that rely on sensitive config.
You can use [`dmno run`](/docs/reference/cli/run/) to inject resolved config into other scripts as regular environment vars.
### HTML Env Replacement
Vite [natively supports](https://vitejs.dev/guide/env-and-mode#html-env-replacement) injecting env vars into HTML files using a special syntax like `%SOME_VAR%`.
This plugin injects additional replacements for strings like `%DMNO_PUBLIC_CONFIG.SOME_VAR%`.
Note that unlike the native functionality which does not replace missing/non-existant items, we will try to replace all items, and will throw helpful errors if something goes wrong.
HTML comments
Note that replacements anywhere in the file, including HTML comments, are still attempted and can cause errors. For example `` will still fail!
### SSR and server-side code
Unlike our [Astro](/docs/integrations/astro/) and [Remix](/docs/integrations/remix/) integrations, if you are using vanilla Vite to do SSR or build backend code, we cannot automatically infer the right way to inject dmno. In this case you may need to include an additional import that initializes the DMNO globals and security features, and run your script via `dmno run` - similar to the [Node.js integration](/docs/integrations/node/).
In fact, if you don’t need build-time replacements or dev server reloading, you may not need this plugin at all.
src/main.ts
```ts
import 'dmno/auto-inject-globals'; // should be imported first!
// rest of your code...
```
package.json
```json
{
// ...
"scripts": {
"start": "dmno run -- node dist/main.js",
},
```
# Using DMNO with Cloudflare
> Use DMNO while deploying on Cloudflare Workers and Pages
At DMNO we *love* [Cloudflare](https://cloudflare.com/). This very site is hosted on it! That’s why we’re excited to make it easier and safer to manage config and secrets in all of your Cloudflare projects.
This platform integration exposes premade schemas and underlying types to interact with env vars related to Cloudflare, as well as a special cli wrapper around `wrangler` that helps deal with config. Aside from all the usual benefits of DMNO - validation, type-safety, sync with backends like 1Password, sharing config across a monorepo - our Cloudflare integration has a few extra tricks up its sleeve:
* configure `wrangler` using DMNO, injecting built-in env vars, and special new ones that are passed in via flags
* inject config into Cloudflare Workers - during both local dev and deployment
* enable DMNO security features in workers - leak prevention, log redaction
* handle both static and dynamic config
* add type-safety to your config, without needing to run `wrangler types`
* access your config anywhere, not just within route handlers
## Setup
* npm
```bash
npm add @dmno/cloudflare-platform
```
* pnpm
```bash
pnpm add @dmno/cloudflare-platform
```
* Yarn
```bash
yarn add @dmno/cloudflare-platform
```
* Bun
```bash
bun add @dmno/cloudflare-platform
```
Note
If you run into any issues, feel free to [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=platforms%2Fcloudflare%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A) or hop in our [Discord](https://chat.dmno.dev).
## Configuring `wrangler` using DMNO
Many facets of the `wrangler` CLI, including authentication, can be set using [system environment variables](https://developers.cloudflare.com/workers/wrangler/system-environment-variables/). By default, if you have a `.env` file in your repo, `wrangler` will automatically use it. But, this suffers from all the usual headaches of using a `.env` file. Instead, DMNO provides a premade vendor schema and associated types so you can inject validated settings into `wrangler` without resorting to a gitignored `.env` file. Instead, you can pull your sensitive Cloudflare API keys from any DMNO plugin, like [1Password](/docs/plugins/1password/) or an [encrypted vault](/docs/plugins/encrypted-vault/), share values across a monorepo, or compose your config however you see fit.
Because `wrangler` is configured using a mix of `wrangler.toml`, env vars, CLI flags, and has no concept of plugins, we also provide a wrapper CLI, called `dwrangler`, that handles everything automatically.
For example, if you wanted to pull your Cloudflare credentials from 1Password, configure dev options, and inject your config, your `.dmno/config.mts` might look like this:
.dmno/config.mts
```ts
import { CloudflareWranglerEnvSchema, DmnoWranglerEnvSchema } from '@dmno/cloudflare-platform';
import { OnePasswordDmnoPlugin } from '@dmno/1password-plugin';
import { DmnoBaseTypes, defineDmnoService, pickFromSchemaObject, switchBy } from 'dmno';
// initialize our 1Password plugin
const opSecrets = new OnePasswordDmnoPlugin('1pass', {
fallbackToCliBasedAuth: true,
});
export default defineDmnoService({
schema: {
// config that affects wrangler directly
...pickFromSchemaObject(CloudflareWranglerEnvSchema, {
CLOUDFLARE_ACCOUNT_ID: {
value: opSecrets.itemByReference('op://Shared/Cloudflare/account id'),
},
CLOUDFLARE_API_TOKEN: {
value: opSecrets.itemByReference('op://Shared/Cloudflare/workers api token'),
},
}),
// special config that controls wrangler via `dwrangler` cli wrapper (all optional)
...pickFromSchemaObject(DmnoWranglerEnvSchema, {
WRANGLER_ENV: {}, // passed as --env
WRANGLER_DEV_IP: { value: 'custom.host.local' }, // passed as --ip
WRANGLER_DEV_PORT: { value: 8881 }, // passed as --port
WRANGLER_DEV_URL: {}, // will be populated with full dev URL
WRANGLER_LIVE_RELOAD: { value: true }, // passed as `--live-reload`
WRANGLER_DEV_ACTIVE: {}, // true when running `dwrangler dev` or `dwrangler pages dev`
WRANGLER_BUILD_ACTIVE: {}, // true when dwrangler is performing a build for deployment
}),
// ... rest of your app config
SOME_VAR: {
value: switchBy('WRANGLER_DEV_ACTIVE', { // use info from wrangler to affect other config
_default: 'dev value',
false: 'prod value',
}),
},
},
});
```
To take advantage of this new config, you swap usage of `wrangler` to `dwrangler`, whether calling it directly or in your `package.json` scripts.
package.json
```json
{
"scripts": {
"dev": "dwrangler dev",
"deploy": "dwrangler deploy"
}
}
```
Don’t worry, `dwrangler` is a simple wrapper
There’s not too much magic going on. It’s a wrapper script that puts config into env vars, flags, and handles smart reloading in `dev` mode.
## Cloudflare Workers
Dealing with [config in Cloudflare Workers](https://developers.cloudflare.com/workers/configuration/environment-variables/) is a bit different than other JS/TS environments. Instead of relying on a global `process.env`, config is passed in as *bindings* to route handlers. These values can be either *vars* (not-sensitive, plaintext) or *secrets* (sensitive, encrypted), and can be set in a variety of ways for local development and for deployments:
* `wrangler --var` CLI option - sets vars during local dev, also sets non-sensitive vars during `wrangler deploy`
* `wrangler.toml` in a `[vars]` section - sets vars, can also be varied per environment (e.g., `[staging.vars]`)
* `.dev.vars` file - set sensitive secrets during local development only
* `wrangler secret` - command to set secrets remotely, also see [`wrangler versions secret`](https://developers.cloudflare.com/workers/wrangler/commands/#secret-put-1) to handle versioned deploys with secrets
Additionally, if not relying on a custom build, `wrangler` internally uses [esbuild](https://esbuild.github.io), and you can do build-time replacements, which can also be used for configuration purposes:
* `wrangler.toml` in a `[define]` section - static vars, can also be varied per environment (e.g., `[staging.define]`)
* `wrangler --define` CLI option - does static replacements during the build, both locally and during deploys
Navigating all of this can be tricky, so we built this integration to make it as easy as possible. Define your DMNO config, and we take care of the rest, giving you a unified way to access your configuration, regardless of if it is [static or dynamic](/docs/guides/dynamic-config/), and sensitive or not.
### Injection via *inline* mode
While it is not very *Cloudflare-y*, our preferred way to inject DMNO config is by inlining the entire resolved config during the build/deploy process. While it may feel a little odd, config changes always trigger a new deployment anyway.
Doing it this way has a few important benefits:
* validated and coerced config, with additional metadata, and it is directly a part of a specific version of your worker
* all config can now be accessed *everywhere*, just like `process.env`, not just within request handlers
* type-safety and IntelliSense on your config, without re-running `wrangler types`
* enables DMNO’s [security features](/docs/get-started/security/) to prevent secrets from leaking over http responses, requests to other servers, and redact secrets from logs
To set this up, we must import the DMNO globals injector at the top of your main worker entrypoint. This version is specifically made to be compatible with Cloudflare’s edge runtime, and will look for data injected by `dwrangler` at build time.
your-worker.js
```js
import 'dmno/injector-standalone/edge-auto';
console.log(DMNO_CONFIG.SOME_ITEM); // 🎉 config is now available everywhere!
export default {
async fetch(request, env, ctx) {
return new Response(`API host: ${DMNO_CONFIG.API_HOST}`);
},
};
```
Potential downsides
There are 2 downsides of this approach to consider:
* existing uses of `env.SOME_SECRET` will no longer work - **you must use `DMNO_CONFIG.SOME_SECRET` instead**.
* the current version of bundled worker code is accessible within the Cloudflare UI, so your devs with access to your account can access this code and see these secrets
### Injection via *secrets* mode
If you want to continue to use Cloudflare’s built-in secrets functionality, you can instead use the *secrets* injection mode. In this mode, static config items will still be replaced at build time, dynamic config items will be set as [Cloudflare secrets](https://developers.cloudflare.com/workers/configuration/secrets/#secrets-on-deployed-workers) and you will continue to read them from the `env` binding injected into your route handlers.
To enable this mode, you must set the `WRANGLER_INJECT_MODE` in your config to `secrets`:
.dmno/config.mts
```ts
export default defineDmnoService({
schema: {
...pickFromSchemaObject(DmnoWranglerEnvSchema, {
WRANGLER_INJECT_MODE: { value: 'secrets' },
}),
},
});
```
**Pros:**
* secrets are no longer bundled into your code
* existing calls to `env.SOME_KEY` will continue to work
**Cons:**
* we cannot activate DMNO’s security features - sensitive secrets can appear in logs, and easily be leaked
* you still cannot access config outside of route handlers
* secrets will always be injected as strings
* less reliable types
To make things a bit more convenient, we do replace references to `DMNO_CONFIG.SOME_VAR` to `env.SOME_VAR`, which means you get automatic type-safety and IntelliSense, and you never have to think about whether a config item is static or dynamic.
You must use the `env` convention
This replacement means that to use `DMNO_CONFIG` and take advantage of the type safety it provides, you must stick with the convention of naming the bindings argument of your route handlers `env`, and if you pass it around to other functions, you should also stick with the name.
my-worker.js
```js
export default {
async fetch(request, env, ctx) {
someHelper(env);
return new Response(`API host: ${DMNO_CONFIG.API_HOST}`);
},
};
function someHelper(env) {
return DMNO_CONFIG.ANOTHER_ITEM;
}
```
While it should all feel seamless, running `dwrangler deploy` in this mode calls multiple commands to do a 3-part deployment:
1. build and upload a new versioned deploy, without activating it
2. upload resolved config as encrypted secrets, creating another version with secrets attached
3. activate the new version with correct secrets attached
### Which mode should you use?
We think for most users the *inline* mode is the better choice, but both options have their merits. Here is a comparison table to help:
| Inline mode | Secrets mode |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| 👍 Static config injected at build time, accessible anywhere | < same |
| ✨ Dynamic config acessible outside route handlers! | 😪 Dynamic config only accessible within route handlers must use `env` naming convention |
| 🔐 Log redaction, leak detection! | 😢 No DMNO security features |
| 🙊 Bundled worker code is accessible within the Cloudflare UI | 🙈 Cloudflare secrets are never visible within the Cloudflare UI |
| 🛠️ Additional installation code | 🛠️ More complex deployment |
### Using `dwrangler` with the Workers Builds git integration
Cloudflare’s new [Workers Builds (beta)](https://developers.cloudflare.com/workers/ci-cd/builds/) allows you to connect your worker to a git repo, and it will run the CI process within Cloudflare, similar to how it works for Pages.
While this is great, it’s not much different than running your CI anywhere else, and the same issues of injecting config into your running worker are present.
To use `dmno` within Workers Builds, just swap your deploy command from `wrangler` to `dwrangler`, just like if you were running it anywhere else. There is also a section for setting “Build variables and secrets” where you can populate any additional config needed during the build. If you are using DMNO to fetch sensitive config from somehwere else, this is how you would pass in that *secret-zero*, for example, a service account token for 1Password.
No preview deployments
At this time, Workers Builds has no notion of deploy previews for PRs or branches. Your only option is to create another worker and point the build settings at a specific branch.
## Cloudflare Pages
Cloudflare pages allows you to host static sites and provides a little sugar on top of Cloudflare Workers for attached functions. Unfortunately, Wrangler’s Pages functionality does not allow the same level of configuration of the underlying ESBuild process. However, it may not matter because, in practice, most users are likely using an existing framework and already have their own build process. In this case, you can rely on our [drop-in integrations](/docs/integrations/overview/) to inject your DMNO config.
Just use workers!
Workers support hosting [static assets](https://developers.cloudflare.com/workers/static-assets/) too and it seems that the Cloudflare team is working to make Workers do everything that Pages can. Our hunch is that Pages will eventually be deprecated, and moving over to Workers will be a smart bet.
Regardless, you could still use DMNO to resolve your config and upload secrets to Cloudflare, but without a custom build, you would have to rely on `env` rather than `DMNO_CONFIG`, and you would not get all the benefits of DMNO.
Please reach out if you need help setting things up, or have a use case that is not supported.
### Cloudflare Pages env vars
The Cloudflare Pages environment injects a few [environment variables](https://developers.cloudflare.com/pages/configuration/build-configuration/#environment-variables) into its *build environment* that provide information about the current build. This module exposes a pre-made config schema object which you can use in your own schema. You can use the `pickFromSchemaObject` utility to pick only the env var keys that you need from the full list. For example:
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { CloudflarePagesEnvSchema } from '@dmno/cloudflare-platform';
export default defineDmnoService({
schema: {
...pickFromSchemaObject(CloudflarePagesEnvSchema, 'CONTEXT', 'BUILD_ID'),
APP_ENV: {
value: switchBy('CONTEXT', {
_default: 'local',
'deploy-preview': 'staging',
'branch-deploy': 'staging',
production: 'production',
}),
},
},
});
```
## Other workflows
If you have a totally custom setup, that does not fit with the above workflows, you can still use `dwrangler` to manage Cloudflare auth, and push resolved config to Cloudflare however you want.
Note that `wrangler` has several bulk secret related methods, and they all take JSON from stdin.
* [`wrangler secret:bulk`](https://developers.cloudflare.com/workers/wrangler/commands/#secretbulk)
* [`wrangler versions secret bulk`](https://developers.cloudflare.com/workers/wrangler/commands/#secret-bulk-1)
* [`wrangler pages secret bulk`](https://developers.cloudflare.com/workers/wrangler/commands/#secret-bulk)
For example: `dwrangler secrets:bulk < dmno resolve --format json`
# Using DMNO with GitHub Actions
> Use DMNO's GitHub Action to reuse your env vars across your GitHub Actions workflows
While DMNO provides everything you need to manage your env vars in your repo, you may want to reuse your env vars across your GitHub Actions workflows. We created a [GitHub Action](https://github.com/marketplace/actions/dmno-secrets-and-config) that makes this easier.
Tip
If you only need to use DMNO, and associated env vars, in a single workflow step then you probably don’t need this action.
## Getting Started Workflow
.github/workflows/my-workflow\.yml
```yaml
name: My Workflow
on:
# replace with your own trigger(s)
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checkout your repo
- uses: actions/checkout@v4
# setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Install dependencies
- name: Install dependencies
run: npm install
# Run DMNO to get your env vars
- uses: dmno/dmno-gh-action@v1
id: dmnoStep
with:
service-name: 'my-dmno-service'
- id: nextStep
run: |
# Use the env var in your workflow
echo $MY_ENV_VAR
```
## Configuration
### Inputs
All inputs are optional!
| Name | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `service-name` | Explicitly select the service to populate config for *useful in a monorepo with multiple services* |
| `emit-env-vars` | Whether to emit environment variables *defaults to true* |
| `output-vars` | Whether to also provide the config as a [job output](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) *defaults to false* |
| `skip-regex` | Use a regex to skip certain config items from being included |
| `skip-cache` | Whether to skip the cache *defaults to false* |
| `clear-cache` | Whether to clear the cache *defaults to false* |
### Outputs
If `emit-env-vars` is `true`, each of your config variables will be emitted as an environment variable.
If `output-vars` is `true`, `DMNO_CONFIG` is output as a JSON string of key-value pairs of the generated variables after being processed by the `skip-regex` regular expression.
## Additional Workflow Examples
We’ll use the following DMNO service as an example for all of the following workflows:
.dmno/config.mts
```typescript
import { defineDmnoService } from 'dmno';
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
// token will be injected using types by default
const onePassSecrets = new OnePasswordDmnoPlugin('1pass');
export default defineDmnoService({
name: 'my-dmno-service',
schema: {
MY_ENV_VAR: {
value: 'some-value',
},
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
},
ITEM_FROM_1PASS: {
value: onePassSecrets.itemByReference('op://vaultname/itemname/path'),
},
}
});
```
### Using the config in a multi-step jobs
This is a common example where you can re-use the environment variables in a subsequent step in the same job. By default, each item in your schema will be emitted as an environment variable.
You can use the config in a multi-step job like this:
.github/workflows/my-multi-step-job.yml
```yaml
name: My Multi-Step Job Workflow
on:
# replace with your own trigger(s)
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checkout your repo
- uses: actions/checkout@v4
# setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Install dependencies
- name: Install dependencies
run: npm install
# Run the DMNO action to get your env vars
- uses: dmno/dmno-gh-action@v1
id: dmnoStep
with:
service-name: 'my-dmno-service'
# Use the env var in the next step
- id: lastStep
run: |
echo $MY_ENV_VAR
```
### Using the config in a multi-job workflow
Multi-job workflows are slightly more complex since each job will have its own set of environment variables. This means you will need to explicity set the outputs you need to use in any other jobs via the `needs` block.
.github/workflows/my-multi-job-workflow\.yml
```yaml
name: My Multi-Job Workflow
on:
# replace with your own trigger(s)
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
outputs:
# the single env var we want to use
MY_ENV_VAR: ${{ steps.lastStep.outputs.MY_ENV_VAR }}
# full stringified JSON of all env vars
DMNO_CONFIG: ${{ steps.dmnoStep.outputs.DMNO_CONFIG }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm install
- uses: dmno/dmno-gh-action@v1
id: dmnoStep
with:
service-name: 'my-dmno-service'
output-vars: true
- id: lastStep
run: |
echo "MY_ENV_VAR=$MY_ENV_VAR" >> "$GITHUB_OUTPUT"
after_build:
runs-on: ubuntu-latest
needs: build
steps:
- run: echo {{ needs.build.outputs.MY_ENV_VAR }}
- run: echo {{ needs.build.outputs.DMNO_CONFIG }}
```
### Using with a DMNO plugin (1Password)
If you’re using a plugin that requires a sensitive input, you can set the input as a secret in GitHub and then pass it to the action as an environment variable with the `env` block. In most cases, this means you will only need to set a single secret via GitHub and let DMNO handle loading the rest.
In this example, we’re using the [1Password plugin](/docs/plugins/overview/) but the workflow is similar for any plugin that requires a sensitive input.
.github/workflows/my-1password-workflow\.yml
```yaml
name: My 1Password Workflow
on:
# replace with your own trigger(s)
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm install
- uses: dmno/dmno-gh-action@v1
id: dmnoWith1Password
env:
# 1Password service account token, set as a secret in GitHub
OP_TOKEN: ${{ secrets.OP_TOKEN }}
with:
service-name: 'my-dmno-service'
- id: nextStep
run: |
# Use the item from 1Password in your workflow
echo $ITEM_FROM_1PASS
```
## Troubleshooting
Make sure:
* DMNO is installed and [set up](/docs/get-started/quickstart/) in your repo
* You have a `.dmno/config.mts` with a [valid schema](/docs/guides/schema/)
* Your action checks out the repo (e.g., `actions/checkout@v4`)
* Your action installs dependencies (e.g., `npm install`)
* You have set any required sensitive [plugin](/docs/plugins/overview/) inputs (e.g., Your 1Password service account token) as [secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) for your repo or as [environment variables](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-for-github-actions#using-environment-variables-in-your-workflow) in your repo. (See example above.)
Note
If you run into any issues, feel free to [report them to us on GitHub](https://github.com/dmno-dev/dmno-gh-action/issues/new?assignees=philmillman\&labels=bug%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A) and try the manual installation steps below.
# Using DMNO with Netlify
> Use DMNO while deploying on Netlify
This platform integration exposes a pre-made config schema and underlying types to interact with the env vars that Netlify injects while on their platform.
It also exposes a Netlify build plugin - which injects your resolved DMNO config into functions and edge functions. You may not need to use this plugin if you already have a build process that injects config values into your functions code before deployment.
## Setup
* npm
```bash
npm add @dmno/netlify-platform
```
* pnpm
```bash
pnpm add @dmno/netlify-platform
```
* Yarn
```bash
yarn add @dmno/netlify-platform
```
* Bun
```bash
bun add @dmno/netlify-platform
```
Note
If you run into any issues, feel free to [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=integrations%2Fastro%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A) and try the manual installation steps below.
## Netlify Config Schema
Netlify injects a set of [read-only environment variables](https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables) during its build process that provide information about the current build.
The `@dmno/netlify-platform` module exposes DMNO data types and a pre-made config schema object which you can use in your own schema. You can use the `pickFromSchemaObject` utility to pick only the env var keys that you need from the full list that Netlify injects. For example:
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { NetlifyEnvSchema } from '@dmno/netlify-platform/types';
export default defineDmnoService({
schema: {
...pickFromSchemaObject(NetlifyEnvSchema, 'CONTEXT', 'BUILD_ID'),
APP_ENV: {
value: switchBy('CONTEXT', {
_default: 'local',
'deploy-preview': 'staging',
'branch-deploy': 'staging',
production: 'production',
}),
},
},
});
```
Import path
Note the import of `@dmno/netlify-platform/types` has an unusual `/types` ending! This is because the Netlify build plugin (see below) must be the main export of the module.
Netlify configuration variables
There are also a set of [Netlify Configuration Variables](https://docs.netlify.com/configure-builds/environment-variables/#netlify-configuration-variables) that affect the Netlify platform and the build process itself. These **cannot** be set by DMNO, because they are loaded by the Netlify platform before the build process even begins, before DMNO is loaded.
## Netlify build plugin
This package also includes a [Netlify build plugin](https://docs.netlify.com/integrations/build-plugins/) which automatically injects your resolved DMNO config into your [Functions](https://docs.netlify.com/functions/overview/) and [Edge Functions](https://docs.netlify.com/functions/overview/).
You may not need this!
This plugin is potentially only needed if you’re using Functions or Edge Functions. If you are deploying a static site then it’s not necessary.
You may also not need it for functions if you are running your own build process and have already handled bundling your DMNO config as static replacements at build time. In this case, you need to make sure that your config is injected into your build command - either via one of our integrations, or by using [`dmno run`](/docs/reference/cli/run/). If your build command is working locally, it probably will just work on Netlify.
You may need to use this plugin if:
* you are authoring functions directly in `netlify/functions` and/or `netlify/edge-functions` folder(s) and you are relying on the netlify cli (`netlify build`) to bundle your code
* you are using an external functions integration that handles creating/bundling your functions code, for example the [Astro Netlify adapter](https://docs.astro.build/en/guides/integrations-guide/netlify/)
Feel free to [reach out on Discord](https://chat.dmno.dev) if you need help.
### Build plugin installation
If you determine that you do need this build plugin, after installing the package itself, you need to add the plugin to your `netlify.toml` file. For example:
netlify.toml
```toml
# ...rest of your config
[[plugins]]
package = "@dmno/netlify-platform"
```
If you are authoring functions directly in `netlify/functions`/`netlify/edge-functions` folder(s), you must add an additional import if you want your functions to be able to run locally using `netlify dev`.
netlify/functions/example-fn.ts
```ts
import '../../.netlify/inject-dmno-config.js';
import type { Context, Config } from "@netlify/functions"
export default async (req: Request, context: Context) => {
console.log(DMNO_CONFIG.SOME_VAR);
// ...
```
If you are using an integration which already builds functions for you - like the [Astro Netlify adapter](https://docs.astro.build/en/guides/integrations-guide/netlify/) - then you do not need to do anything.
## Migrating from Netlify managed config
**Should I set env vars in the Netlify UI at all?**
Of course you *can* still set overrides and secrets within the Netlify UI if you like and rely on config values being injected that way.
However, we recommend migrating all of your config to DMNO itself by using [switchBy](/docs/reference/helper-methods/#switchby) to create branching logic based on the env vars injected by Netlify.
You can also use plugins - for example our [Encrypted Vault](/docs/plugins/encrypted-vault/) and [1Password](/docs/plugins/1password/) plugins - to handle sensitive config within your schema. Note that you will still set a **single environment variable** in the Netlify UI, to allow these plugins to access the rest of your secrets.
For example:
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { NetlifyEnvSchema } from '@dmno/netlify-platform/types';
import { EncryptedVaultDmnoPlugin, EncryptedVaultTypes } from '@dmno/encrypted-vault-plugin';
// you could use a single vault, but it's best practice
// to split out prod secrets to limit access
const DevSecretsVault = new EncryptedVaultDmnoPlugin('vault/dev', {
key: configPath('..', 'DMNO_VAULT_KEY_DEV'),
name: 'dev',
});
const ProdSecretsVault = new EncryptedVaultDmnoPlugin('vault/prod', {
key: configPath('..', 'DMNO_VAULT_KEY_PROD'),
name: 'prod',
});
export default defineDmnoService({
schema: {
DMNO_VAULT_KEY_DEV: { extends: EncryptedVaultTypes.encryptionKey },
DMNO_VAULT_KEY_PROD: { extends: EncryptedVaultTypes.encryptionKey },
...pickFromSchemaObject(NetlifyEnvSchema, 'CONTEXT'),
APP_ENV: {
value: switchBy('CONTEXT', {
_default: 'local',
'deploy-preview': 'staging',
'branch-deploy': 'staging',
production: 'production',
}),
},
SOME_API_KEY: {
value: switchBy('APP_ENV', {
_default: 'not-sensitive-dev-key',
staging: DevSecretsVault.item(),
production: ProdSecretsVault.item(),
}),
},
},
});
```
### Scopes
Note that Netlify’s built-in environment variable tooling has a concept of [scopes](https://docs.netlify.com/environment-variables/overview/#scopes) and certain vars being applied/available at different times, for example during the build versus during function execution. There are also some limitations around env vars being injected into functions due to the nature of how those platforms are set up.
Using DMNO, we avoid all of that since we inject all of your *resolved* DMNO config at build time, and standardize access to your config regardless of the situation.
Note that any env vars set in the Netlify UI or `netlify.toml` must include the `build` scope to be noticed by DMNO - since we inject your config at build time.
# Platforms overview
> Use DMNO with your favorite deployment platforms and tools including: Netlify, Vercel, Render.com, and more.
We publish first-party packages to help you use DMNO with your favourite deployment platforms.
In many cases, you may not need to do anything special, but our platform-specific config schemas will give you a nicer experience using the environment variables that your platform may inject while running your code. For example:

### [Cloudflare](/docs/platforms/cloudflare/)
Package: `@dmno/cloudflare-platform`
Includes:
* Cloudflare-specific data types
* Config schema of env vars injected by the Cloudflare platform
* `dwrangler` cli - wrapper around `wrangler` to inject DMNO config
### [Netlify](/docs/platforms/netlify/)
Package: `@dmno/netlify-platform`
Includes:
* Config schema of env vars injected by the Netlify platform
* Netlify-specific data types
* Netlify build plugin
### [Vercel](/docs/platforms/vercel/)
Package: `@dmno/vercel-platform`
Includes:
* Config schema of env vars injected by the Vercel platform
* Vercel-specific data types
### Next up
We’re already working on more platform integrations, but we’d love to hear from you which platforms to tackle next!
On the roadmap:
* Fly.io
* Heroku
* Render.com
# Using DMNO with Vercel
> Use DMNO while deploying on Vercel
This platform integration exposes a pre-made config schema and underlying types to interact with the env vars that Vercel injects while using their platform. You may not need this at all, but it can be useful if you are using their platform extensively and want helpful type info for their system’s env vars.
Tip
There is a Project Setting in the Vercel UI where you can turn off these env vars, so make sure that “Automatically expose System Environment Variables” is enabled.
## Setup
* npm
```bash
npm add @dmno/vercel-platform
```
* pnpm
```bash
pnpm add @dmno/vercel-platform
```
* Yarn
```bash
yarn add @dmno/vercel-platform
```
* Bun
```bash
bun add @dmno/vercel-platform
```
Note
If you run into any issues, feel free to [report them to us on GitHub](https://github.com/dmno-dev/dmno/issues/new?assignees=philmillman\&labels=integrations%2Fvercel%2Cbug%2Ctriage\&template=bug_report.md\&template=bug_report.yml\&title=%5BBUG%5D%3A) and try the manual installation steps below.
## Vercel Config Schema
Vercel injects a set of [system environment variables](https://vercel.com/docs/projects/environment-variables/system-environment-variables) at build and runtime that provide information about the current build or runtime environment.
The `@dmno/vercel-platform` module exposes DMNO data types and a pre-made config schema object which you can use in your own schema. You can use the `pickFromSchemaObject` utility to pick only the env var keys that you need from the full list that Vercel injects. For example:
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { VercelEnvSchema } from '@dmno/vercel-platform';
export default defineDmnoService({
schema: {
...pickFromSchemaObject(VercelEnvSchema, 'VERCEL_ENV', 'VERCEL_GIT_COMMIT_REF'),
// example of adding more specificity/control over env flag using vercel's env vars
APP_ENV: {
value: () => {
if (DMNO_CONFIG.VERCEL_ENV === 'production') return 'production';
if (DMNO_CONFIG.VERCEL_ENV === 'preview') {
if (DMNO_CONFIG.VERCEL_GIT_COMMIT_REF === 'staging') return 'staging';
else return 'preview';
}
return 'development';
},
},
},
});
```
Vercel configuration variables
There are also a set of [Framework specific env vars](https://vercel.com/docs/projects/environment-variables/system-environment-variables#framework-environment-variables) that vercel injects to expose their env vars to the framework. Using DMNO, you can just access those vars and mark them as non-sensitive so there is no need for them.
## Migrating from Vercel managed config
**Should I set env vars in the Vercel UI at all?**
Vercel has the ability to set config vars per environment - dev/preview/prod. Of course you *can* still set overrides and secrets within the Vercel UI if you like and rely on config values being injected that way.
However, we recommend migrating all of your config to DMNO itself by using [switchBy](/docs/reference/helper-methods/#switchby) to create branching logic based on the `VERCEL_ENV` flag injected and any other logic you like. This is much more flexible and powerful, and will centralize your config management within your codebase.
You can also use plugins - for example our [Encrypted Vault](/docs/plugins/encrypted-vault/) and [1Password](/docs/plugins/1password/) plugins - to handle sensitive config within your schema. Note that you will still set a **single environment variable** in the Vercel UI, to allow these plugins to access the rest of your secrets.
For example:
.dmno/config.mts
```ts
import { defineDmnoService, switchBy, pickFromSchemaObject } from 'dmno';
import { VercelEnvSchema } from '@dmno/vercel-platform';
import { EncryptedVaultDmnoPlugin, EncryptedVaultTypes } from '@dmno/encrypted-vault-plugin';
// you could use a single vault, but it's best practice
// to split out prod secrets to limit access
const DevSecretsVault = new EncryptedVaultDmnoPlugin('vault/dev', {
key: configPath('..', 'DMNO_VAULT_KEY_DEV'),
name: 'dev',
});
const ProdSecretsVault = new EncryptedVaultDmnoPlugin('vault/prod', {
key: configPath('..', 'DMNO_VAULT_KEY_PROD'),
name: 'prod',
});
export default defineDmnoService({
schema: {
DMNO_VAULT_KEY_DEV: { extends: EncryptedVaultTypes.encryptionKey },
DMNO_VAULT_KEY_PROD: { extends: EncryptedVaultTypes.encryptionKey },
...pickFromSchemaObject(VercelEnvSchema, 'VERCEL_ENV'),
SOME_API_KEY: {
value: switchBy('VERCEL_ENV', {
_default: 'not-sensitive-dev-key',
staging: DevSecretsVault.item(),
production: ProdSecretsVault.item(),
}),
},
},
});
```
# 1Password plugin
> DMNO's 1Password plugin provides additional guard-rails and improved DX when using application config stored in 1Password.
DMNO’s [1Password](https://1password.com/) plugin allows you to securely integrate your secrets stored in 1Password into the rest of the DX improvements DMNO provides. This plugin uses their [JavaScript SDK](https://github.com/1Password/onepassword-sdk-js/) to authenticate using a [service account](https://developer.1password.com/docs/service-accounts). Additionally, for local development, you can opt-in to use your system-installed [1Password CLI](https://developer.1password.com/docs/cli/get-started/) and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). This plugin is compatible with any 1Password account type (personal, family, teams, business), but note that [rate limits](https://developer.1password.com/docs/service-accounts/rate-limits/) vary by account type.
## Installation & setup
Install the package in the service(s) that will use config from 1Password.
* npm
```bash
npm add @dmno/1password-plugin
```
* pnpm
```bash
pnpm add @dmno/1password-plugin
```
* Yarn
```bash
yarn add @dmno/1password-plugin
```
* Bun
```bash
bun add @dmno/1password-plugin
```
***
After installation, you’ll need to initialize the plugin in your dmno config and add a 1Password service account token into your config schema. You can explicitly wire the plugin up to the service account token if using multiple tokens at once, or it will be injected by default. It’s ok if you have not created this service account yet - we’ll do that in the next section.
.dmno/config.mts
```ts
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
// token will be injected using types by default
const onePassSecrets = new OnePasswordDmnoPlugin('1pass');
// or you can wire up the path explicitly
const onePassSecrets2 = new OnePasswordDmnoPlugin('1passWithExplicitPath', {
token: configPath('..', 'OP_TOKEN'),
});
export default defineDmnoService({
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
// NOTE - the type itself is already marked as sensitive 🔐
},
},
});
```
Plugin instance IDs
You must give each plugin instance a unique id so we can refer to it in other services and the [`dmno` CLI](/docs/reference/cli/plugin/).
See [Segmenting secrets](/docs/guides/secret-segmentation/) for more details.
***
## Setup vault & service account
If you already use 1Password and your secrets live in a vault that holds other important passwords and info, you should create a new vault and move your secrets to it, because **the access system of 1Password is based on vaults, not individual items**.
1. **Create a vault** in your 1Password account which will be used to hold your secrets. You can create multiple vaults to segment access to different environments, services, etc. This can be done using any 1Password app, the web app, or the CLI. [link](https://support.1password.com/create-share-vaults/#create-a-vault)
2. **Create a new service account** and grant access to necessary vault(s). This is a special account used for machine-to-machine communication. This can only be done in the 1Password web interface. Be sure to copy the new service account token or save it in another vault. [link](https://developer.1password.com/docs/service-accounts/get-started/)
Vault access set during creation only
Vault access rules cannot be edited after creation, so if your vault setup changes, you will need to create new service account(s) and update the tokens.
3. **Grant vault access to users/teams (optional)**. Your developers may need access to at least some of your vaults, especially if using the `op` cli based auth mentioned below. [link](https://support.1password.com/create-share-vaults-teams/#share-a-vault)
4. **Ensure vault service account access is enabled (optional)**. Each vault has a toggle to disable service account access *in general*. It is on by default, so you will likely not need to do anything. [link](https://developer.1password.com/docs/service-accounts/manage-service-accounts/#manage-access)
This service account token will now serve as your *secret-zero* - which grants access to the rest of your sensitive config stored in 1Password. It must be set locally (unless relying on cli-based auth) and in any deployed environments. It is sensitive so we must pass in the value as an *override* rather than storing it within the config. See our [overrides guide](/docs/guides/overrides/) for more details.
Vault organization best practices
Consider how you want to organize your vaults and service accounts, keeping in mind [best practices](https://support.1password.com/business-security-practices/#access-management-and-the-principle-of-least-privilege). At a minimum, we recommend having a vault for highly sensitive production secrets and another for everything else.
### Desktop app / CLI integration (optional)
During local development, you may find it convenient to skip the service account tokens and instead rely on your system’s `op` CLI and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). This means you will be connecting to 1Password as if you were using your local 1Password desktop application, including using its biometric unlocking features.
1. **Opt-in while initializing the plugin**
.dmno/config.mts
```ts
const onePassSecrets = new OnePasswordDmnoPlugin('1pass/dev', {
token: configPath('..', 'OP_TOKEN'),
fallbackToCliBasedAuth: true,
});
```
*Of course you can also point to a `configPath` in your schema and toggle the opt-in based on some other logic if you’d like.*
2. **Ensure the `op` CLI is installed**. [docs](https://developer.1password.com/docs/cli/get-started/)
3. **Enable the desktop app + CLI integration**. [docs](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration)
4. **Run `op signin` to sign in on the CLI**. Ensure you are logged in to the correct account. You can run `op whoami` to see which account is currently connected to the CLI.
With this option enabled, if the resolved service account token is empty, we will call out to the `op` cli installed on your machine (it must be in your `$PATH`) and use the auth it provides. With the desktop app integration enabled, it will call out and may trigger biometric verification to unlock. It is secure and very convenient!
Connecting as yourself
Keep in mind that this method is connecting as *YOU* who likely has more access than a tightly scoped service account. Consider only enabling this method for a plugin instance that will be handling non-production secrets.
***
## Add items to your schema
With the plugin initialized and access wired up, now we must update our config schema to connect specific config values to data stored in 1Password. DMNO supports a few different ways to reference items in 1Password:
### Using a `.env` blob
Managing lots of individual 1Password items and connecting them to your config can be a bit tedious. So, when getting started, we recommend storing multiple items together in a `.env` style text blob. Using this method, we’ll have a single 1Password item that can have one text entry per service containing the `.env` blob and look up items by their key - similar to applying a `.env.local` file as overrides, except they are secured and shared via 1Password. This also makes it easier to migrate from passing around `.env` files.
1. **Create a new item within your vault**. Select `Secure Note` as the item type and be sure to give it a descriptive name (e.g., `Prod secrets`).
2. **Create a new field in the item**. Click `+ add more` and select `Text` to add a new multi-line text field. Change the default label of `text` to the [service name](/docs/guides/schema/#service-name) you want to store secrets for (e.g., `root`). You can also use the special name `_default` if you are only dealing with a single service.
Multiple services
In a monorepo, you can initialize a single plugin instance in your root service and inject it into each child service. In this case, add a field for each service.
3. **Add your secrets to the text field** as if it was another `.env` file that would be loaded as overrides. For example:
```plaintext
SOME_API_KEY=super-secret-key
ANOTHER_ITEM="quotes work too"
```
*You can also come back and do this later.*
4. **Wire up plugin instance to the new item** using its *private link*. While viewing the item in the 1Password app, click the 3 dots in the top right and click `Copy Private Link`. As this link does not contain anything sensitive, we can use a static value as our plugin input.
5. **Update items in your config schema** to use the `.item()` value resolver for anything that will be stored in the linked 1Password item. When we resolve your config values, if a match is not found, it will result in a `ResolutionError` with helpful info about how to fix it.
Your dmno config should end up looking like this:
.dmno/config.mts
```ts
const onePassSecrets = new OnePasswordDmnoPlugin('1pass/prod', {
token: configPath('..', 'OP_TOKEN'),
envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
});
export default defineDmnoService({
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
},
SOME_API_KEY: {
sensitive: true,
value: onePassSecrets.item(),
}
},
});
```
And your 1Password item may look this:

Key lookup details and example
Values are looked up within the linked 1Password item using a simple convention. We expect to find a *text field* within the item with a label set to the current [service name](/docs/guides/schema/#service-name). The contents of that item are parsed as a [`.env` file](https://dotenvx.com/docs/env-file), and we look up items using the config item key. If no match is found, we will also look in an additional field with the label `_default`.
For example, in the item above, an item with the key `ONE_MORE` would fallback to the value in the `_default` field in any service that wasn’t named `root`.
You can also override the key used to lookup the value in the `.env` blob. This can be useful if you need to save multiple values toggled by some other logic.
.dmno/config.mts
```typescript
export default defineDmnoService({
schema: {
SOME_API_SECRET: {
sensitive: true,
value: switchBy('APP_ENV', {
// uses the default key of "SOME_API_SECRET"
_default: onePassSecretsDev.item(),
// uses overridden key
staging: onePassSecretsDev.item('SOME_API_SECRET_STAGING'),
// uses the default key but looking in a different 1pass item
staging: onePassSecretsProduction.item(),
}),
},
},
});
```
### Using specific 1Password items
If you already have lots of individual items in 1Password, or you just don’t want to use the blob method, we provide several methods to wire up individual config items to specific values in 1Password. Note that while 1Password reference URIs (e.g., `op://vaultname/itemname/path`) are easier to use in some ways, they are based on field labels and are not stable, so the other methods are preferred.
```ts
export default defineDmnoService({
schema: {
// using item private link
ITEM_WITH_LINK: {
value: onePassSecrets.itemByLink(
'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
'somefieldid',
),
},
// using UUIDs
ITEM_WITH_IDS: {
value: onePassSecrets.itemById('vaultUuid', 'itemUuid', 'somefieldid'),
},
// using item reference url
ITEM_WITH_REFERENCE: {
value: onePassSecrets.itemByReference('op://vaultname/itemname/path'),
},
},
});
```
Where to find an item private link
You can find the private link by clicking the 3 dots **on the item** in the 1Password interface and selecting `Copy Private Link`.
Where to find field IDs
Field IDs are not easy to get from the 1Password UI. Luckily when the supplied field ID is not found, our error message includes a list of all the possible IDs in the item. Start with an empty string or a bogus ID like `"?"` and use the DMNO error message to find the right field ID.
Where to find an item reference
The secret reference for invidivual fields within an item can be found by clicking on the down arrow icon **on the field** and selecting `Copy Secret Reference`.
Active config iteration
While you are actively working on the config itself, `dmno resolve -w --skip-cache` will combine watch mode with skipping cache logic.
See [caching](/docs/plugins/overview/#caching) for more details.
## Override loader
Aside from wiring up individual items to 1Password in your schema, you can also use the `onePasswordOverrideLoader` to load values from a 1Password as [overrides](/docs/guides/overrides/). This is the best way to avoid ever needing to use `.env` files, which means you’ll never have anything sensitive sitting in plaintext on your machine - and all secrets will always be secured by biometric authentication.
In this example, we’ll use two instances of the `onePasswordOverrideLoader` - one for personal overrides, and another to load in a group of shared config without having to wire up the individual items:
.dmno/config.mts
```ts
import { defineDmnoService, processEnvOverrideLoader } from 'dmno';
import { OnePasswordDmnoPlugin, OnePasswordTypes, onePasswordOverrideLoader } from '@dmno/1password-plugin';
export default defineDmnoService({
name: 'api',
overrides: [
processEnvOverrideLoader(),
// personal overrides
// each dev can have a matching item in their personal "Employee" vault
onePasswordOverrideLoader(
{ reference: 'op://Employee/myapp-local-dev-overrides/api' },
{ ignoreMissing: true } // do not throw if item doesn't exist
),
// shared overrides
onePasswordOverrideLoader({ reference: 'op://non-prod-config/local-dev-env/api' }),
],
//...
```
# Bitwarden plugin
> DMNO's Bitwarden plugin allows you to securely access your secrets stored in Bitwarden Secrets Manager.
This DMNO plugin allows you to securely access your secrets stored in [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/). Please note that this plugin is **not compatible with Bitwarden’s Password Manager product**. Authentication with Bitwarden uses [Machine Account Access Tokens](https://bitwarden.com/help/access-tokens/).
## Installation & setup
Install the package in the service(s) that will use secrets from Bitwarden.
* npm
```bash
npm add @dmno/bitwarden-plugin
```
* pnpm
```bash
pnpm add @dmno/bitwarden-plugin
```
* Yarn
```bash
yarn add @dmno/bitwarden-plugin
```
* Bun
```bash
bun add @dmno/bitwarden-plugin
```
***
After installation, you’ll need to initialize the plugin in your `config.mts` and add a config item to hold your machine account access token. You can explicitly wire the plugin up to the service account token if using multiple tokens at once, or it will be injected by default based on the `BitwardenSecretsManagerTypes.machineAccountAccessToken` type. It’s ok if you have not created the machine account or access token - we’ll do that in the next section.
.dmno/config.mts
```ts
import { BitwardenSecretsManagerDmnoPlugin, BitwardenSecretsManagerTypes } from '@dmno/bitwarden-plugin';
// by default, access token will be injected using types
const bitwardenPlugin = new BitwardenSecretsManagerDmnoPlugin('bitwarden');
// or you can explicitly wire it up by path
const bitwardenPlugin2 = new BitwardenSecretsManagerDmnoPlugin('bitwarden', {
accessToken: configPath('..', 'BWS_TOKEN')
});
export default defineDmnoService({
schema: {
BWS_TOKEN: {
extends: BitwardenSecretsManagerTypes.machineAccountAccessToken,
// NOTE - the type itself is already marked as sensitive 🔐
},
},
});
```
Plugin instance IDs
You must give each plugin instance a unique id so we can refer to it in other services and the [`dmno` CLI](/docs/reference/cli/plugin/).
In this case we used `bitwarden`, but you can imagine splitting vaults and access, and having multiple plugin instances - for example `bitwarden/prod` for highly sensitive production secrets and `bitwarden/dev` for everything else.
***
## Setup Project & Secrets
If you are already using Bitwarden Secrets Manager, you likely already have existing [projects](https://bitwarden.com/help/projects/) that contain [secrets](https://bitwarden.com/help/secrets/). If so, now would be a good time to review how they are all organized. If not, you should create at least one project, as each secret can have a parent project it belongs to, and access can be granted to projects rather than managing each secret individually.
Use projects to segment access
You should use multiple projects to segment your secrets following the [Principle of Least Privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). See more in our [Secret segmentation](/docs/guides/secret-segmentation/) guide.
Machine account access tokens now serve as your *secret-zero* - which grants access to the rest of your sensitive config stored in Bitwarden. It must be set locally and in deployed environments, but it is sensitive so we must pass in the value as an *override* rather than storing it within the config. Locally, this usually means storing it in your [`.env.local` file](/docs/guides/env-files/) and on a deployed environment you’ll usually set it wherever you would normally pass in environment variables. DMNO will handle the rest. See [Setting overrides](/docs/guides/env-files/#overrides) for more details.
***
## Add items to your schema
With the plugin initialized and access wired up, now we must update our config schema to connect specific config values to data stored in Bitwarden secrets.
Items are wired up using the secret UUIDs found in the Bitwarden UI. For example:
```ts
export default defineDmnoService({
schema: {
ITEM_WITH_ID: {
value: bitwardenPlugin.secretById('abc123-secretuuid-xyz789'),
},
// example showing a switchBy and multiple plugin instances
SWITCHED_ITEM: {
value: switchBy('MY_ENV_FLAG', {
_default: 'not-sensitive',
staging: bitwardenDevSecrets.secretById('0123...'),
production: bitwardenProdSecrets.secretById('789...'),
}),
},
},
});
```
## Self-hosted
In case you are self-hosting Bitwarden Secrets Manager, the `BitwardenSecretsManagerDmnoPlugin` also takes additional inputs for `apiServerUrl` and `identityServerUrl`. The values for this can be found in the Bitwarden UI under `Machine Accounts` > `Config`. See the [Bitwarden docs](https://bitwarden.com/help/machine-accounts/#configuration-information) for more details.
```typescript
const bitwardenPlugin = new BitwardenSecretsManagerDmnoPlugin('bitwarden', {
apiServerUrl: 'https://vault.bitwarden.com/api', // default value
identityServerUrl: 'https://vault.bitwarden.com/identity', // default value
});
```
# DMNO Encrypted Vaults
> Store secrets securely in an encrypted vault that you check in to your git repo with DMNO Config.
## Install
Install the package:
* npm
```bash
npm add @dmno/encrypted-vault-plugin
```
* pnpm
```bash
pnpm add @dmno/encrypted-vault-plugin
```
* Yarn
```bash
yarn add @dmno/encrypted-vault-plugin
```
* Bun
```bash
bun add @dmno/encrypted-vault-plugin
```
Tip
Run this in the root if you want the plugin to be shared, otherwise run it in the specific service’s directory.
## Initialize the plugin
Initialize the plugin in the root, or service if not shared. Note the `vault/prod` id, which we can refer to in other services or in the CLI. This is useful if you have multiple vaults.
Also, note the `configPath` function. This is a helper to wire up the encryption key plugin input to the location of that value in your service’s config. We obviously don’t want to hardcode that key in this file, so this lets it live within the rest of our config, and pull in the value from a `.env.local` file or environment variable. The plugin has it’s own internal config schema, so the first argument of `'..'` tells us to look in the plugin’s parent - the service.
\[root]/.dmno/config.mts
```typescript
import { defineDmnoService, configPath } from 'dmno';
import { EncryptedVaultDmnoPlugin, EncryptedVaultTypes } from '@dmno/encrypted-vault-plugin';
const MyProdVault = new EncryptedVaultDmnoPlugin('vault/prod', {
key: configPath('..', 'DMNO_VAULT_KEY'),
});
export default defineDmnoService({
schema: {
DMNO_VAULT_KEY: {
extends: EncryptedVaultTypes.encryptionKey,
// NOTE - the type itself is already marked as secret
},
},
});
```
If your plugin was initiatized in root and you need to use in a child service, inject the already configured plugin:
services/child-service/.dmno/config.mts
```typescript
import { EncryptedVaultDmnoPlugin } from '@dmno/encrypted-vault-plugin';
const MyVault = EncryptedVaultDmnoPlugin.injectInstance('vault/prod'); // same "instance name" it was created with
```
Note
Note that we’re referencing the plugin by the id we gave it (`'vault/prod'`) when we initialized it. This is necessary when we have to reference multiple instances of a plugin. For example. if you’re segmenting them by environment.
***
## Initialize the vault and key
* npm
```bash
npm exec -- dmno plugin -p vault/prod -- setup
```
* pnpm
```bash
pnpm exec dmno plugin -p vault/prod -- setup
```
* Yarn
```bash
yarn exec -- dmno plugin -p vault/prod -- setup
```
* Bun
```bash
bun run dmno plugin -p vault/prod -- setup
```
Tip
If you prefer to use the CLI interactively, you can simply run `pnpm exec dmno plugin` and follow the prompts.
This will:
* detect if the vault is configured but has no key value
* detect if vault file is empty/exists
* create new a key if needed
## Add vault items to your schema
config.mts
```typescript
{
// simple case example
SUPER_SECRET_ITEM: {
value: MyProdVault.item(),
},
ITEM_WITH_PROD_ONLY_SECRET: {
value: toggleByNodeEnv({
_default: 'not-a-secret',
staging: NonProdVault.item(), // reference to another vault
production: MyProdVault.item(),
})
},
}
```
## Fill the vault with your secrets
Add encrypted values to the vault:
* npm
```bash
npm exec -- dmno plugin -p vault -- add
```
* pnpm
```bash
pnpm exec dmno plugin -p vault -- add
```
* Yarn
```bash
yarn exec -- dmno plugin -p vault -- add
```
* Bun
```bash
bun run dmno plugin -p vault -- add
```
## Rotate the vault key
* npm
```bash
npm exec -- dmno plugin -p vault -- rotate-key
```
* pnpm
```bash
pnpm exec dmno plugin -p vault -- rotate-key
```
* Yarn
```bash
yarn exec -- dmno plugin -p vault -- rotate-key
```
* Bun
```bash
bun run dmno plugin -p vault -- rotate-key
```
This will:
* generate a new key, and share it, similar to the initial setup
* re-encrypts all the values in the vault with the new key
## Accessing an existing vault
If you’re joining a project that already has a vault set up, you’ll will need to get the key from a coworker.
Tip
Coming soon, you will be able to use the CLI to request the key from a coworker.
## Plugin CLI reference
## Reference
*Description:* *Runs CLI commands related to a specific plugin instance*
#### Options
```plaintext
-s, --service [service]
```
*which service to load*
***
```plaintext
-np, --no-prompt
```
*do not prompt for service selection*
***
```plaintext
-p, --plugin
```
*which plugin instance to interact with*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# set up a new encrypted vault
dmno plugin -p vault -- setup
# Update or insert an item to te vault
dmno plugin -p vault -- upsert
# add an item to the vault
dmno plugin -p vault -- add
# update an item in the vault
dmno plugin -p vault -- update
# delete an item from the vault
dmno plugin -p vault -- delete
# delete an item from the vault
dmno plugin -p vault -- delete
```

# Infisical plugin
> DMNO's Infisical plugin allows you to securely access your secrets stored in Infisical.
This DMNO plugin allows you to securely access your secrets stored in [Infisical](https://infisical.com/). The current implementation uses [Machine Identities](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) and [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth#universal-auth). If you need to use a different authentication method, please [open an issue](https://github.com/dmnojs/dmno/issues) and we can discuss options.
## DMNO installation & setup
Install the package in the service(s) that will use secrets from Infisical.
* npm
```bash
npm add @dmno/infisical-plugin
```
* pnpm
```bash
pnpm add @dmno/infisical-plugin
```
* Yarn
```bash
yarn add @dmno/infisical-plugin
```
* Bun
```bash
bun add @dmno/infisical-plugin
```
***
After installation, you’ll need to initialize the plugin in your `config.mts` and add a few config items that are necessary to authenticate with Infisical and fetch secrets. It’s ok if you have not created the machine identity or client keys - we’ll do that in the next section.
.dmno/config.mts
```ts
import { InfisicalDmnoPlugin, InfisicalTypes } from '@dmno/infisical-plugin';
// explicitly wire the plugin instance to the config path
const infisicalPlugin = new InfisicalDmnoPlugin('infisical/dev', {
environment: 'development',
clientId: configPath('..', 'INFISICAL_CLIENT_ID'),
clientSecret: configPath('..', 'INFISICAL_CLIENT_SECRET'),
projectId: configPath('..', 'INFISICAL_PROJECT_ID'),
});
// or you can inject by default
const infisicalPlugin2 = new InfisicalDmnoPlugin('infisical/prod', {
environment: 'production',
});
export default defineDmnoService({
schema: {
// ...
INFISICAL_CLIENT_ID: { extends: InfisicalTypes.clientId },
INFISICAL_CLIENT_SECRET: { extends: InfisicalTypes.clientSecret },
INFISICAL_PROJECT_ID: { extends: InfisicalTypes.projectId },
// ...
},
});
```
Wiring up plugin instances
See our [General plugin guidelines](/docs/plugins/overview/#general-plugin-guidelines) for more information on how to wire up plugin instances.
***
## Infisical setup
### Project & secrets
If you are an existing Infisical user, you probably already have projects and secrets. If not, you should create at least one [project](https://infisical.com/docs/documentation/platform/project). Infisical uses the concept of [environments](https://infisical.com/docs/documentation/platform/project#project-environments) to group secrets. Make sure to make your secrets available in the same environment configuration as each plugin instance.
### Machine identity & client keys
Next, you’ll need to create a [Machine Identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#concept) in your **Organization** under **Access Control**. Make note of the **Client ID** and create a new **Client Secret**. Then in your project, make sure the identity you created has the necessary access. This is configured in the project settings under the **Access Control** -> **Machine Identities** tab.
How you want to segment your identities and secrets is up to you. You could create a separate identity and secrets for each environment, or each service, or each project. At minimum, we recommend segmenting your production and non-production secrets. See [Secret Segmentation](/docs/guides/secret-segmentation/) for more details.
Also note that the **Client Secret** is highly sensitive and should be treated as your *secret zero*. It will need to be set locally and passed in as an override. Locally, it can be set in your `.env.local` file, and in any deployed environments it can be set however you normally set environment variables for that platform. DMNO will handle the rest. See [Setting overrides](/docs/guides/env-files/#overrides) for more details.
***
## Adding items to your schema
The Infisical plugin provides one method for fetching secrets, based on the name of the secret. The name itself will be inferred from the config item name. You can optionally pass a name if you wish to override the default.
.dmno/config.mts
```typescript
import { InfisicalDmnoPlugin, InfisicalTypes } from '@dmno/infisical-plugin';
const infisicalPlugin = new InfisicalDmnoPlugin('infisical/dev', {
environment: 'development',
});
export default defineDmnoService({
schema: {
SOME_SECRET: {
// this will fetch the secret with the name 'SOME_SECRET' from the project specified in the plugin instance
value: infisicalPlugin.secret(),
},
SOME_NEW_SECRET: {
// this will fetch the secret with the name 'SOME_OTHER_SECRET' and make it available as SOME_NEW_SECRET in your DMNO_CONFIG
value: infisicalPlugin.secret('SOME_OTHER_SECRET'),
},
},
});
```
### Self-hosted Infisical
If you are using a self-hosted version of Infisical, the `InfisicalDmnoPlugin` takes an optional `siteUrl` parameter. For example:
.dmno/config.mts
```typescript
import { InfisicalDmnoPlugin, InfisicalTypes } from '@dmno/infisical-plugin';
const infisicalPlugin = new InfisicalDmnoPlugin('infisical/dev', {
environment: 'development',
siteUrl: 'https://infisical.mycompany.com',
});
```
# Plugins
> DMNO's extensible plugin system allows you to add new functionality to your DMNO configuration. Plugins can be used to add new features, add reusable types, integrate with third-party services, or generally extend the core functionality of DMNO.
DMNO’s extensible plugin system allows you to add new functionality to your DMNO configuration. Plugins can be used to add new features, add reusable types, integrate with third-party services, or generally extend the core functionality of DMNO.
### Available plugins
Our first set of plugins are designed to help you manage secrets:
* [**DMNO Encrypted Secrets Plugin**](/docs/plugins/encrypted-vault/): Store secrets securely in an encrypted vault file that you check in to your git repo.
* [**DMNO 1Password Plugin**](/docs/plugins/1password/): Securely pull secrets from [1Password](https://1password.com/)
* [**DMNO Bitwarden Secrets Manager Plugin**](/docs/plugins/bitwarden/): Securely pull secrets from [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/)
* [**DMNO Infisical Plugin**](/docs/plugins/infisical/): Securely pull secrets from [Infisical](https://infisical.com/)
## General plugin guidelines
### Plugin inputs
In general, plugins are initialized with a set of inputs that are used to configure the plugin. These inputs usually live in the `schema` of the DMNO service where the plugin is initialized. While the exact inputs will vary by plugin, they will often include some combination of:
* Sensitive values that are needed to access the plugin’s service
* Non-sensitive values that are needed to configure the plugin
Because plugins are typically used to access sensitive items themselves, this introduces a slight chicken-and-egg problem. In this case, DMNO allows you to define the plugin inputs in the `schema` but not provide a value. The sensitive value is then set via an *override*.
More on overrides
For more on how overrides work and how to use them, see the [Setting overrides via `.env` files](/docs/guides/env-files/#overrides).
### Wiring up the inputs
Because DMNO provides plugin-specific types, we can automatically inject the correct type for the plugin input. This means you don’t need to manually wire up the inputs when you initialize the plugin. Our config engine will do this for you.
For example:
```ts
import { SomePlugin, SomePluginTypes } from '@dmno/some-plugin';
// by default, access token will be injected using types
const somePluginInstance = new SomePlugin('some-plugin');
// or you can explicitly wire it up by path
const somePluginInstance2 = new SomePlugin('some-plugin', {
someToken: configPath('..', 'SOME_TOKEN'),
someOtherToken: configPath('..', 'SOME_OTHER_TOKEN'),
});
export default defineDmnoService({
schema: {
SOME_TOKEN: {
// this type allows us to inject the correct value from the config item
extends: SomePluginTypes.someToken,
},
SOME_OTHER_TOKEN: {
extends: SomePluginTypes.someOtherToken,
},
},
});
```
Be careful
Because of the nature of type-based injection, you can only do this for a single config item of a given type. If you have multiple config items of the same type, like two of the same type of token for two different plugin instances, you’ll need to explicitly wire them up.
### Multiple plugin instances
Plugins support multiple instances allowing you to compose your configuration in a flexible way. For example, with the 1Password plugin you could create separate instances for your production and non-production vaults. Each instance can have its own settings, allowing you to manage per-environment or per-service secrets.
When defining a plugin instance, you provide an `id` which will be used to identify the instance in various places, such as the CLI, UI, and when injecting it into other services.
### Injecting plugin instances in monorepo services
In a monorepo, you might want to use the same plugin instance across multiple services. If you will be using the same settings for each service, you can initialize a plugin instance once in a parent service as seen above, and then inject it in child services. This alleviates the need to have the necessary config in each additional service. Note that the injected plugin instance must use the same id we set during initialization.
apps/some-service/.dmno/config.mts
```typescript
import { SomePlugin } from '@dmno/some-plugin';
// 💉 inject the already initialized plugin instead of re-initializing it
const someInjectedPluginInstance = SomePlugin.injectInstance('some-plugin');
```
Plugin instances are great for secret segmentation
To read more about secret segmentation, see the [Secrets guide](/docs/guides/secret-segmentation/).
## Caching
In order to avoid rate limits and keep dev server restarts extremely fast, we heavily cache data fetched from external sources. After updating secrets from any plugin that stores them externally, if the item has been cached, you’ll need to clear the cache to see it take effect.
* Use the [`dmno clear-cache` command](/docs/reference/cli/clear-cache/) to clear the cache once
* The [`dmno resolve`](/docs/reference/cli/resolve/) and [`dmno run`](/docs/reference/cli/run/) commands have cache related flags:
* `--skip-cache` - skips caching logic altogether
* `--clear-cache` - clears the cache once before continuing as normal
Active config iteration
While you are actively working on the config itself, `dmno resolve -w --skip-cache` will combine watch mode with skipping cache logic.
Once you are satisfied, clear the cache once more and you are good to go.
Note
We will soon be opening up our APIs for developers to create their own DMNO plugins. If you’d like to be part of that, drop us a line via [email](mailto:hello@dmno.dev) or join our [Discord](https://chat.dmno.dev).
# DMNO base types
> DMNO comes with a comprehensive set of types to cover the majority of use cases. These types are used when defining your config schema and can be extended to create more application specific types as needed.
## Primitive types
DMNO comes with a comprehensive set of types to cover the majority of use cases.
These types are used when defining your config schema and can be extended to create more application specific types as needed.
See [*creating your own types*](/docs/reference/helper-methods/#createdmnodatatype) for more on this.
### `string`
`DmnoBaseTypes.string({ settingsSchema? })`
DMNO Built-in data type for Strings. Includes the following optional settings:
* Schema
```ts
type StringDataTypeSettings =
```
* Properties
No properties
Examples
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
MY_STRING: DmnoBaseTypes.string({
minLength: 2,
maxLength: 5,
}),
MY_STRING2: DmnoBaseTypes.string({
isLength: 5,
}),
MY_STRING3: DmnoBaseTypes.string({
startsWith: 'pk_',
}),
// ...
},
// ...
});
```
### `number`
`DmnoBaseTypes.number({ settingsSchema? })`
DMNO Built-in data type for Numbers. Includes the following optional settings:
* Schema
```ts
type NumberDataTypeSettings =
```
* Properties
No properties
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
MY_NUMBER: DmnoBaseTypes.number({
min: 2,
max: 5,
}),
// ...
},
// ...
});
```
### `boolean`
`DmnoBaseTypes.boolean()`
DMNO Built-in data type for Booleans.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
MY_BOOLEAN: DmnoBaseTypes.boolean(),
// ...
},
// ...
});
```
### `enum`
`DmnoBaseTypes.enum({ settingsSchema? })`
DMNO Built-in data type for Enums. Includes the following optional settings:
```typescript
type settingsSchema = {
// simple list of values
values: Array
// array or values with extra metadata
| Array< { value: string | number | boolean, description: string }>
// object with key value pairs (strings only)
| Record;
};
```
Example:
```javascript
const myEnumType = DmnoBaseTypes.enum({ values: ['one', 'two', 'three'] });
const myEnumType2 = DmnoBaseTypes.enum({
values: [
{ value: 'one', description: 'The first one' },
{ value: 'two', description: 'The second one' }],
});
const myEnumType3 = DmnoBaseTypes.enum(
{
values: {
one: 'The first one',
two: 'The second one',
},
},
);
```
## Composite types
### `email`
`DmnoBaseTypes.email({ settingsSchema? })`
DMNO Built-in data type for Email addresses.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
EMAIL: DmnoBaseTypes.email({
normalize: true,
}),
// ...
},
// ...
});
```
### `url`
`DmnoBaseTypes.url({ settingsSchema? })`
DMNO Built-in data type for URLs.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
URL: DmnoBaseTypes.url({
prependProtocol: true, // adds https:// if missing
}),
// ...
},
// ...
});
```
### `ipAddress`
`DmnoBaseTypes.ipAddress({ settingsSchema? })`
DMNO Built-in data type for IP Addresses.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
IP: DmnoBaseTypes.ipAddress({
version: 4, // or 6
}),
// ...
},
// ...
});
```
### `port`
`DmnoBaseTypes.port({ settingsSchema? })`
DMNO Built-in data type for Ports.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
PORT: DmnoBaseTypes.port({
min: 1024, // > 0
max: 49151, // < 65535
}),
// ...
},
// ...
});
```
### `semver`
`DmnoBaseTypes.semver()`
DMNO Built-in data type for Semantic Versioning.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
VERSION: DmnoBaseTypes.semver(),
// ...
},
// ...
});
```
### `isoDate`
`DmnoBaseTypes.isoDate()`
DMNO Built-in data type for ISO Dates. Ex. `2022-01-01T00:00:00.000Z`.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
DATE: DmnoBaseTypes.isoDate(),
// ...
},
// ...
});
```
### `uuid`
`DmnoBaseTypes.uuid()`
DMNO Built-in data type for UUIDs.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
UUID: DmnoBaseTypes.uuid(),
// ...
},
// ...
});
```
### `md5`
`DmnoBaseTypes.md5()`
DMNO Built-in data type for MD5 Hashes.
Example:
```javascript
import { DmnoBaseTypes, defineDmnoService } from 'dmno';
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
MD5: DmnoBaseTypes.md5(),
// ...
},
// ...
});
```
### `NodeEnvType`
DMNO Built-in data type for `NODE_ENV`, built using `Enum`.
Its definition looks like this:
```javascript
const NodeEnvType = createDmnoDataType({
extends: DmnoBaseTypes.enum({
development: { description: 'true during local development' },
test: { description: 'true while running tests' },
production: { description: 'true for production' },
}),
});
```
Example:
```javascript
import { NodeEnvType, defineDmnoService } from 'dmno';
// ...
export default defineDmnoService({
name: 'MyConfig',
// ...
schema: {
NODE_ENV: NodeEnvType,
// ...
},
// ...
});
```
Advanced
In the event that our base types aren’t sufficient, you can extend them to create your own using `createDmnoDataType`, see more [here](/docs/reference/helper-methods/#createdmnodatatype).
# dmno clear-cache
> Clear the cache using the `dmno clear-cache` command.
## Reference
*Description:* *Tools to clear / reset the cache Also note many commands have \`--skip-cache\` and \`--clear-cache\` flags*
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# Clear the entire cache
dmno clear-cache
```

# DMNO CLI
> The `dmno` cli provides a number of commands to help you run and manage your DMNO projects.
The `dmno` cli was installed when you [installed](/docs/get-started/quickstart/) `dmno` and is available as a depedency in your project.
The `dmno` cli provides a number of commands to help you run and manage your DMNO projects.
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
### Commands
* [`dmno init`](/docs/reference/cli/init/) - Initialize a new DMNO project
* [`dmno resolve`](/docs/reference/cli/resolve/) - Load the resolved config for a service
* [`dmno dev`](/docs/reference/cli/dev/) - Run a service in dev mode, watching for changes and updates
* [`dmno run`](/docs/reference/cli/run/) - Run a command with the resolved config for a service
* [`dmno plugin`](/docs/reference/cli/plugin/) - Run plugin specific commands
* [`dmno clear-cache`](/docs/reference/cli/clear-cache/) - Clear the local DMNO cache
# dmno dev
> Start a development server using the `dmno dev` command.
## Reference
*Description:* *Runs the service in dev mode, and watches for changes and updates as needed.*
#### Options
```plaintext
--silent
```
*do not log anything, useful when using in conjunction with a ConfigServerClient which will do its own logging*
***
```plaintext
--ipc-only
```
*skip booting the local web server, and communicate over IPC only*
***
```plaintext
--skip-cache
```
*skips config cache altogether, will not read or write*
***
```plaintext
--clear-cache
```
*clears the cache before continuing, will write new values to cache*
***
```plaintext
-s, --service [service]
```
*which service to load*
***
```plaintext
-np, --no-prompt
```
*do not prompt for service selection*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# Runs the service in dev mode
dmno dev
```

# dmno init
> Initialize a new DMNO project using the `dmno init` command.
`dmno init` is the easiest way to get started with DMNO.
It will automatically detect your project type and install the necessary packages and configuration for you. This includes setting up your `config.mts` file, and creating a `.dmno` directory in the root of your project. It will also import any config items from your `.env` files and add them to the `schema` in your `config.mts` files. Finally, it will install and configure any necessary integrations for your project type.
## Reference
*Description:* *Sets up dmno in your repo, and can help add to new packages within your monorepo - safe to run multiple times*
#### Options
```plaintext
--silent
```
*automatically select defaults and do not prompt for any input*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# Set up dmno and uses interactive menus to make selections
dmno init
```

# dmno plugin
> Manage plugins using the `dmno plugin` command.
## Reference
*Description:* *Runs CLI commands related to a specific plugin instance*
#### Options
```plaintext
-s, --service [service]
```
*which service to load*
***
```plaintext
-np, --no-prompt
```
*do not prompt for service selection*
***
```plaintext
-p, --plugin
```
*which plugin instance to interact with*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# set up a new encrypted vault
dmno plugin -p vault -- setup
# Update or insert an item to te vault
dmno plugin -p vault -- upsert
# add an item to the vault
dmno plugin -p vault -- add
# update an item in the vault
dmno plugin -p vault -- update
# delete an item from the vault
dmno plugin -p vault -- delete
# delete an item from the vault
dmno plugin -p vault -- delete
```

# dmno resolve
> Load the resolved config for a service using the `dmno resolve` command.
## Reference
*Description:* *Loads the resolved config for a service*
#### Options
```plaintext
-f,--format
```
*format to output resolved config (ex. json)*
***
```plaintext
--public
```
*only loads public (non-sensitive) values*
***
```plaintext
--show-all
```
*shows all items, even when config is failing*
***
```plaintext
-w,--watch
```
*watch for config changes and re-run*
***
```plaintext
-p,--phase
```
*resolve in specific phase*
***
```plaintext
--skip-cache
```
*skips config cache altogether, will not read or write*
***
```plaintext
--clear-cache
```
*clears the cache before continuing, will write new values to cache*
***
```plaintext
-s, --service [service]
```
*which service to load*
***
```plaintext
-np, --no-prompt
```
*do not prompt for service selection*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# Loads the resolved config for the root service
dmno resolve
# Loads the resolved config for service1
dmno resolve --service service1
# Loads the resolved config for service1 in JSON format
dmno resolve --service service1 --format json
# Loads the resolved config for service1 and outputs it in .env file format
dmno resolve --service service1 --format env
# Loads the resolved config for service1 and outputs it in .env file format and writes to .env.local
dmno resolve --service service1 --format env >> .env.local
```

# dmno run
> Run a command in the context of a service using the `dmno run` command.
## Reference
*Description:* *Runs a command with the resolved config for a service*
#### Options
```plaintext
-w,--watch
```
*watch for config changes and re-run*
***
```plaintext
-p,--phase
```
*resolve in specific phase*
***
```plaintext
--skip-cache
```
*skips config cache altogether, will not read or write*
***
```plaintext
--clear-cache
```
*clears the cache before continuing, will write new values to cache*
***
```plaintext
-s, --service [service]
```
*which service to load*
***
```plaintext
-np, --no-prompt
```
*do not prompt for service selection*
***
PATH & node\_modules/.bin
The `dmno` cli is installed as a depedency in your project and is available in your `node_modules/.bin` directory. Generally the best way to run it is via your package manager, for example `pnpm exec dmno`. For simplicity’s sake, we will omit the `pnpm exec`/`pnpm dlx` prefix in the examples below.
#### Example(s)
```bash
# Runs the echo command with the resolved config for service1
dmno run --service service1 -- printenv $SOME_ITEM
# Runs the somecommand with the resolved config using SOME_VAR via printenv
dmno run —-service service1 -- somecommand --some-option=(printenv SOME_VAR)
```

Caution
While `dmno run -- echo $SOME_ITEM` seems like it should work, your shell will try to resolve that variable *before* dmno injects your resolved config as env vars.
Instead you must use `printenv` - for example `dmno run -- printenv SOME_ITEM`.
# Example Reference
> A reference page in my new Starlight docs site.
Reference pages are ideal for outlining how things work in terse and clear terms. Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you’re documenting.
## Further reading
* Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework
# Helper methods
> Helper methods for defining your DMNO configuration schema.
### `defineDmnoService`
`defineDmnoService({ opts })`
> See the [service config](/docs/guides/schema/) in the schema guide for more information.
This method is used to define the configuration schema in each of your services, including the root. It takes an object as an argument with the following properties:
* Schema
```ts
type DmnoServiceConfig = {
name?: string,
parent?: string,
tags?: string[],
settings?: {
dynamicConfig: DynamicConfigModes,
},
schema: Record,
};
```
* Properties
| Name | Type | Required | Description |
| -------- | ------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------- |
| name | string | No | The name of the service - will use \`name\` from package.json if not set. |
| parent | string | No | The name of the parent service - will default to root service if not set. |
| tags | string\[] | No | An array of tags for the service. |
| settings | { dynamicConfig: DynamicConfigModes } | No | Settings to apply to the service and as defaults for any children |
| schema | Record\ | Yes | The schema for the workspace. These are the configuration items that will be available to all services. |
#### `DynamicConfigModes`
The `DynamicConfigModes` type has the following values:
```typescript
type DynamicConfigModes =
/* non-sensitive = static, sensitive = dynamic (this is the default) */
'public_static' |
/* everything static, dynamic not supported */
'only_static' |
/* everything dynamic, static not supported */
'only_dynamic' |
/* default is static */
'default_static' |
/* default_dynamic */
'default_dynamic';
```
Tip
For more on dynamic config modes, see the [Dynamic vs static config](/docs/guides/dynamic-config/) guide.
#### `ConfigItemDefinition`
The `ConfigItemDefinition` type is an object with the following properties:
* Schema
```ts
type ConfigItemDefinition = {
asyncValidate?: function(),
coerce?: function(),
description?: string,
dynamic?: boolean,
exampleValue?: any,
extends?: DmnoDataType | string | () => DmnoDataType,
externalDocs?: { description?, url } | Array<{ description?, url }>,
required?: boolean,
sensitive?: boolean,
summary?: string,
typeDescription?: string,
ui?: { color, icon },
useAt?: 'build' | 'boot' | 'run' | 'deploy' ,
validate?: function(),
value?: InlineValueResolverDef
}
```
* Properties
| Name | Type | Required | Description |
| --------------- | ----------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| asyncValidate | function | No | An async function to validate the value. |
| coerce | function | No | A function to coerce the value. |
| description | string | No | A description of the item. |
| dynamic | boolean | No | Whether the item is dynamic. |
| exampleValue | any | No | An example value for the item. |
| extends | DmnoDataType \| string \| () => DmnoDataType | No | A string (the name of the type), an initialized dmno data type, or a function that returns a DmnoDataType |
| externalDocs | { description?, url } \| Array<{ description?, url }> | No | External documentation for the item. |
| required | boolean | No | Whether the item is required. |
| sensitive | boolean | No | Whether the item is sensitive. |
| summary | string | No | A short summary description of the item. |
| typeDescription | string | No | A description of the type. |
| ui | { color, icon } | No | UI settings for the item. COMING SOON |
| useAt | ConfigRequiredAtTypes | No | When the item is required. |
| validate | function | No | A function to validate the value. |
| value | InlineValueResolverDef | No | A static value, a function that returns a static value, or a "resolver" like a plugin helper function that fetches a value. |
Examples illustrating implicit and explicit type extensions:
```typescript
import { defineDmnoService, DmnoBaseTypes } from 'dmno';
export default defineDmnoService({
schema: {
ITEM0: {}, // defaults to string
ITEM1: 'string',
ITEM2: DmnoBaseTypes.string,
ITEM3: DmnoBaseTypes.string(),
ITEM4: {
extends: 'string',
},
ITEM5: {
extends: DmnoBaseTypes.string,
},
ITEM6: {
extends: DmnoBaseTypes.string({}),
},
},
});
```
Did you know?
All of the above examples are equivalent.
### `createDmnoDataType`
`createDmnoDataType({ opts })`
This method is used to create a new data type. It takes an object as an argument with the following properties:
```typescript
type DataTypeOpts = {
name: string;
extends: string | DmnoDataType | (() => DmnoDataType);
settingsSchema?: Record;
validate?: (ctx, settings) => boolean;
coerce?: (ctx, settings) => unknown;
};
```
Example:
```javascript
const myType = createDmnoDataType({
name: 'MyType',
extends: DmnoBaseTypes.string({
// string specific settings object
}),
settingsSchema: {
// user type specific settings object
},
validate: (value) => {
// return true if value is valid
// has access to settingsSchema
},
coerce: (value) => {
// return coerced value
// has access to settingsSchema
},
});
```
You can then use it in your config schema like so:
```javascript
const myType = DmnoBaseTypes.MyType({ settings });
export default defineDmnoService({
name: 'MyConfig',
parent: 'root',
schema: {
MYFIELD: {
extends: myType,
required: true,
},
MYFIELD2,
MYFIELD3,
},
});
```
### `switchBy`
`switchBy('SWITCH_BY_KEY': string, { branches })`
This method is used to define different configurations for different values of a particular config item. Its arguments are a string (i.e., the key name) and an object with the following properties:
```typescript
type branches = {
_default?: any;
[key: string]: any;
};
```
Note: `_default` is a reserved key to represent the default branch. This default will be selected if the current value of the switch is not found on any other branch. All the other keys should match the possible values of the `SWITCH_BY_KEY` config item.
A real example using an enviroment flag:
```javascript
import { switchBy } from 'dmno';
export default defineDmnoService({
schema: {
APP_ENV: {
value: 'development',
},
MY_CONFIG_ITEM: {
value: switchBy('APP_ENV', {
_default: 'dev/default value', // <- matches
test: 'test value',
staging: 'staging value',
production: 'prod value',
}),
},
},
});
```
Default branch is optional
Having a `_default` branch is not required, but if there is no default branch and an unknown value is passed to the switch, you will get a `ResolutionError`. If this is not what you want, you must add `_default: undefined` or add branches for all expected switch values, even if they are set to resolve to `undefined`.
Switching based on environment
While switching based on an environment flag (e.g., dev/staging/prod) is the most common use case for branching logic in your config, we do not recommend using `NODE_ENV` as that flag for most projects. See our [Multiple Environments guide](/docs/guides/multi-env/) for more details.