Skip to main content
This page describes how to run separate development, QA and production environments with the current release. Developers build taps and pipelines in dev, a QA team checks them in qa, and prod runs them on a schedule against real destinations. Code lives in GitHub. Everything here works without code changes. Steps that still need a person are listed under Limits today. In short:

Why three installs

Install Datris three times, once per environment, each in its own DATRIS_DIR on its own host:
  • Each install has its own MongoDB, MinIO, Vault, Postgres, users, agent policy and audit log. The installs share nothing, so the isolation needs no configuration.
  • Compose publishes fixed ports (8080, 4200, 3000, 9000 and others), so running two stacks on one host is not a supported layout.
  • The internal environment prefix (as in oss-data and oss/postgres) is oss by default. Leave it at the default on every install so that secret paths and bucket names in a pipeline carry over from one install to another.
  • Give qa the same instance size as prod so that QA runs are representative. Dev can be smaller and can use the bundled Postgres and MongoDB as destinations.
Why not three catalogs on one install? Catalogs such as dev_sales, qa_sales and prod_sales on one server do not work as environments:
  • Tap and pipeline names are unique across an install, so the same pipeline cannot exist in two catalogs. Promoting would mean renaming, and renaming breaks every reference to the item.
  • Secrets belong to the install, not to a catalog. Dev taps could read production credentials.
  • One install has one agent policy, one audit log, one user list and one upgrade. An experiment in dev can take production down.
Multi-tenant mode (MULTI_TENANT=true) isolates the stores per tenant, but it is meant for separate customers. Its tenants share one server process and one release, and the installer does not set it up.

Make definitions portable

A catalog archive restores cleanly on the next environment only if the definitions in dev were written for it. Follow these rules in dev. Keep everything that differs per environment in a secret. Pipelines and taps refer to secrets by name only. Create the same names on qa and prod and give them each environment’s values in Configuration → Secrets. An archive never carries secret values. The archive lists the secret names the catalog needs. The restore result and datris catalog restore name any of them that do not exist on the target yet. Avoid the four fields that do not carry over. These fields are specific to an environment and cannot come from a secret. If a pipeline sets one of them, the archive and the restore result carry a warning that the value may not be valid on another install:
  • destination.objectStore.endpoint
  • destination.objectStore.destinationBucketOverride
  • destination.database.warehouse (Snowflake warehouse name or Databricks SQL warehouse id)
  • dataQuality.validationSchema when it is an absolute s3:// path
In dev, use the default bucket and a relative validation schema path, and use a warehouse name that exists under the same name in every environment. If a pipeline has to set one of these fields, edit it on the target after each restore. Use one catalog per team or data domain. Do not use one catalog per environment. A catalog such as sales has the same name on all three installs and is promoted as a whole. Two teams that release on different schedules need two catalogs. Uncataloged cannot be exported, so create anything meant for production in a catalog. Keys scoped with pipeline:create:catalog=<name> enforce this for agents. See Data Catalog.

GitHub layout

Use one repository with one branch per environment:
On each install, set Configuration → Code Repository to the same repository and the same path prefix (taps/), and to that environment’s branch: dev, qa or main. Give each install a fine-grained token scoped to that one repository with Contents: Read and write. Setup is described in Tap and pipeline scripts in GitHub. Scripts move from one environment to the next through a pull request:
  1. A developer saves a tap in dev. Datris commits taps/<tap>.py on dev and pins the tap to that commit.
  2. Open a pull request from dev to qa. Reviewers see the script diff in GitHub. Merge it.
  3. Export the catalog from dev and restore it on qa (see Promote a catalog).
  4. Test, enable and run the taps on qa.
  5. Repeat with a pull request from qa to main and a restore on prod.
When you restore, the target compares each archived tap script with the file at the head of its branch: Merge the pull request before you restore. Then the archive matches the branch and the restore leaves the repository unchanged. This comparison covers tap scripts only: AI data-quality and transformation scripts are regenerated on the target, as described under Limits today. Branch protection on main. Saving a tap or an AI script on a GitHub-backed install commits directly to its branch. You can set this up in one of two ways:
  • Prod is GitHub-backed. The prod token must be allowed to push to main, so branch protection on main cannot block that token. Prod reads its scripts from the branch, so the restore comparison above applies to prod as well.
  • Prod uses built-in storage (the default, MinIO). main can be fully protected and only dev and qa use GitHub storage. Prod keeps its scripts in its own object store, and a restore on prod does not compare against main.
Tap and pipeline definitions (schedules, parameters, destinations, rules), validation schemas and tap prompts are not stored in GitHub. They move inside the catalog archive. Committing the archive under catalogs/ gives them a coarse history.

Configure each environment

Set these in each install’s .env (see Configuration Reference): Configure qa the same way as prod. Permission and policy problems then show up in QA, before the prod restore. DATRIS_ENV changes behaviour only when it is set to production: loads to external Postgres then require TLS. The CodeGen model must be the same on all three installs. A restore regenerates AI data-quality and transformation scripts on the target. The script prod runs can only match the one QA tested if both were generated by the same provider and model. Set ai.codegen.* to the same values on every install. Use these roles and keys (see API Keys, Agent Policy and Single sign-on (OIDC)): Create keys from templates and rotate them on a schedule. Issue one key per agent per environment so that the audit log attributes every action to the right agent. On prod, start the agent policy from Use recommended and set every mutating action a prod agent can reach to Approve.

Promote a catalog

These steps promote the sales catalog from dev to qa. Repeat them from qa to prod.
  1. Merge the pull request from dev to qa (see GitHub layout).
  2. Export from the source environment:
  3. Restore on the target environment. Use --overwrite every time; it also works the first time, when the catalog does not exist on the target yet. Use --no-state, because a tap’s incremental cursor records where dev got to, not where qa should start:
    A restore never merges. Overwrite replaces the catalog’s taps and pipelines that are in the archive, and never touches another catalog. Items in the target catalog that are not in the archive are kept. If the restore names missing secrets, create them on the target with that environment’s values before you test the taps.
  4. Restored taps start disabled, so a restore never starts a schedule on its own. Test each tap (datris tap test <name>), then enable it (datris tap update <name> --enabled). When restored taps read each other’s tables, use Enable in order on the catalog card, or have an agent call enable_catalog_ordered. Either one tests, enables and runs the taps in dependency order.
  5. Run the pipelines against the target’s destinations.
Restore refuses the whole archive if any tap or pipeline name in it already exists on the target in a different catalog. The full rules are in Data Catalog and in the CLI reference. A CI promotion job. A GitHub Actions workflow can run steps 2 and 3:
  • The runner must be able to reach each environment’s MCP server. Use a self-hosted runner, or put the MCP port behind the TLS reverse proxy with REQUIRE_API_KEY=true.
  • datris catalog restore exits 1 on a refusal or when an item could not be restored, so the job fails visibly.
  • The job cannot finish the promotion: the restored taps are disabled, and a person tests and enables them (step 4).
  • Keep every archive, either as a build artifact (as above) or committed under catalogs/, so that every prod state can be reproduced.
The approval gate for prod. Under the agent policy, a CI key counts as an agent (What counts as an agent). A restore is checked as pipeline:create. If that action is on Approve in prod, the CI job’s restore waits as a pending approval that nobody polls. Per-resource overrides can only tighten the policy, so you cannot exempt the CI key. Choose one of these:
  • A person restores on prod with their own key. CI promotes to qa only. The prod policy stays strict.
  • The GitHub environment is the gate. Put the prod job behind a GitHub environment with required reviewers, and leave pipeline:create on Auto in the prod policy. Agents on prod can then also create pipelines without approval, so keep the prod agent keys read-only.

Upgrade order

Run the same release on all three installs. Upgrade dev and let it run for a while, then upgrade qa and run one promotion end to end, then upgrade prod. Run datris doctor --pre-upgrade on each install before you upgrade it (see Doctor and the upgrade procedure). Never restore an archive from a newer Datris into an older one. An install refuses an archive whose format is newer than it supports. The archive records the source release but the restore does not check it, so keep the target on a release at least as new as the source.

Point agents and the CLI at an environment

Give each environment its own MCP client entry and its own key. With mcp-remote (see Configuring Claude):
The CLI chooses the install from MCP_SERVER_URL and DATRIS_API_KEY alone (see CLI and MCP Server). Set both per shell, for example with direnv or one shell function per environment:
There is no CLI profile file. The three UIs and the three MCP servers look and respond the same, and nothing shows which environment you are connected to. Name the entries and functions clearly, and give prod keys the fewest capabilities.

Limits today

  • AI data-quality and transformation scripts regenerate on restore with the target’s CodeGen model, and the archived codegen/*.py files are reference copies only, so pin the same CodeGen model on all environments.
  • There is no dry run: restore is all-or-nothing, with no preview of what --overwrite would change.
  • Restored taps start disabled and there is no catalog-level CLI command to test and enable them, so a CI promotion ends with a person (UI Enable in order) or an agent calling enable_catalog_ordered.
  • The agent policy cannot exempt one key: a CI key under Approve queues restore_catalog, so prod CI uses GitHub environment reviewers as its gate.