Skip to content
  • SDKs and APIs
  • Special guides

Manage Kinde configuration as code across environments

Kinde environments are isolated — configuration doesn’t automatically carry over between dev, staging, and production. This guide shows you how to treat your Kinde setup like infrastructure: define it once as JSON, then use a JavaScript seeding script with the Kinde Management API to apply it consistently across environments.

  • A Kinde account with Admin or Engineer access (Sign up for free)
  • An M2M application with Kinde Management API access — see the quick start guide
  • Node.js version 20+

JavaScript seeding script setup

Link to this section

1. Create the seed script

Link to this section
  1. Create the seed script directory and file:

    Terminal window
    mkdir scripts
    touch scripts/seed-kinde.mjs
  2. Enter the following code into the seed script and save changes. Include only the function calls relevant to your use case.

    import fs from "node:fs/promises"
    const {
    KINDE_DOMAIN,
    KINDE_CLIENT_ID,
    KINDE_CLIENT_SECRET,
    KINDE_AUDIENCE,
    KINDE_SCOPES,
    CONFIG_PATH,
    } = process.env
    if (
    !KINDE_DOMAIN ||
    !KINDE_CLIENT_ID ||
    !KINDE_CLIENT_SECRET ||
    !KINDE_AUDIENCE
    ) {
    throw new Error("Missing Kinde env vars")
    }
    const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8"))
    async function getToken() {
    const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: KINDE_CLIENT_ID,
    client_secret: KINDE_CLIENT_SECRET,
    audience: KINDE_AUDIENCE,
    scope: KINDE_SCOPES,
    })
    const res = await fetch(`https://${KINDE_DOMAIN}/oauth2/token`, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body,
    })
    if (!res.ok) {
    const text = await res.text() // <-- see why it failed
    throw new Error(`Token error ${res.status}, ${text}`)
    }
    const json = await res.json()
    return json.access_token
    }
    function client(token) {
    const base = `https://${process.env.KINDE_DOMAIN}`
    async function call(method, path, body) {
    const url = new URL(`/api/v1${path}`, base)
    const res = await fetch(url, {
    method,
    headers: {
    "content-type": "application/json",
    authorization: `Bearer ${token}`,
    },
    body: body ? JSON.stringify(body) : undefined,
    })
    if (!res.ok) {
    throw new Error(`${method} ${base} ${path} -> ${res.status}`)
    }
    return res.status === 204 ? null : res.json()
    }
    return { call }
    }
    // Helper functions
    async function createApplicationWithUrls(
    api,
    name,
    type,
    redirects = [],
    logouts = []
    ) {
    console.log("Creating application...")
    const errors = []
    let appObj = {}
    try {
    appObj = await api.call("POST", "/applications", {
    name,
    type,
    })
    } catch {
    console.log("Cannot create an application, exiting the function")
    return
    }
    const { id } = appObj.application
    console.log("Creating auth redirect URLs...")
    try {
    await api.call("POST", `/applications/${id}/auth_redirect_urls`, {
    urls: redirects,
    })
    } catch (error) {
    errors.push("Cannot create redirect urls")
    }
    console.log("Creating auth logout URLs...")
    try {
    await api.call("POST", `/applications/${id}/auth_logout_urls`, {
    urls: logouts,
    })
    } catch (error) {
    errors.push("Cannot create logout urls")
    }
    console.log("Created a Kinde application with id", id)
    if (errors.length) console.log("with some errors", errors)
    }
    async function createEnvVariable(api, item) {
    console.log("Creating environment variable...")
    try {
    const { key, value, sensitive = false } = item
    await api.call("POST", `/environment_variables`, {
    key,
    value,
    is_secret: sensitive,
    })
    console.log("Created environment variable", item.key)
    } catch {
    console.log("Could not create env variable", item.key)
    }
    }
    async function createFeatureFlag(api, flag) {
    try {
    await api.call("POST", `/feature_flags`, flag)
    console.log("Created feature flag:", flag.name)
    } catch {
    console.log("Could not create feature flag:", flag.name)
    }
    }
    async function createApiAndScopes(api, { name, audience, scopes = [] }) {
    console.log("Creating API and Scopes...")
    let apiObj = null
    try {
    apiObj = await api.call("POST", `/apis`, { name, audience })
    apiObj.name = name
    console.log("Created API:", name)
    } catch {
    console.log("Error creating API:", name)
    return
    }
    // You will need a Kinde paid plan
    // To add scope to your API
    // Un-comment this block to use it:
    // for (const s of scopes) {
    // try {
    // await api.call("POST", `/apis/${apiObj.api.id}/scopes`, {
    // key: s,
    // description: s,
    // })
    // console.log(`Created scope "${s}" to the API ${apiObj.name}`)
    // } catch (error) {
    // console.log("Error creating scope:", s)
    // }
    // }
    }
    async function createRoleAndPermissions(api, role) {
    console.log("Creating roles and permissions...")
    let roleObj = null
    try {
    roleObj = await api.call("POST", `/roles`, {
    key: role.key,
    name: role.name,
    })
    roleObj.name = role.name
    console.log("Created role:", role.name)
    } catch {
    console.log("Error creating role:", role.name)
    console.log("Exiting.")
    return
    }
    const permObjs = []
    let permObj = null
    for (const p of role.permissions) {
    try {
    permObj = await api.call("POST", `/permissions`, {
    key: p.key,
    name: p.name,
    })
    permObj.name = p.name
    console.log("Permission created:", p.name)
    permObjs.push(permObj)
    } catch {
    console.log("Error creating the permission:", p.name)
    }
    }
    for (const perm of permObjs) {
    const { id } = perm.permission
    try {
    await api.call("PATCH", `/roles/${roleObj.role.id}/permissions`, {
    permissions: [{ id }],
    })
    console.log(
    `Added the permission "${perm.name}" to the role ${roleObj.name}`
    )
    } catch {
    console.log("Error creating the permission")
    }
    }
    }
    // Token and create the client
    const token = await getToken()
    const api = client(token)
    // Destructuring the values
    const { name, type, redirectUrls, logoutUrls } = cfg.application
    const { envVars, apis, featureFlags, roles } = cfg
    // Execute the script
    await Promise.all([
    createApplicationWithUrls(api, name, type, redirectUrls, logoutUrls),
    ...(envVars ?? []).map((v) => createEnvVariable(api, v)),
    ...(apis ?? []).map((a) => createApiAndScopes(api, a)),
    ...(featureFlags ?? []).map((f) => createFeatureFlag(api, f)),
    ...(roles ?? []).map((r) => createRoleAndPermissions(api, r)),
    ])
    console.log("Kinde Seed Complete")

2. Create the config files

Link to this section
  1. Create the config files for each environment:

    Terminal window
    mkdir config
    touch config/dev.json config/prod.json config/staging.json
  2. Enter the following seed data into your config files and save changes. Modify the sample data below according to your needs. See the Kinde Management API documentation for available fields.

    {
    "application": {
    "name": "My Kinde App, Created with Script",
    "type": "reg",
    "redirectUrls": ["http://localhost:3000/callback"],
    "logoutUrls": ["http://localhost:3000/"]
    },
    "apis": [
    {
    "key": "orders-api",
    "name": "Orders API",
    "audience": "orders",
    "scopes": ["orders:read", "orders:write"]
    }
    ],
    "roles": [
    {
    "key": "admin",
    "name": "Admin",
    "permissions": [
    { "key": "orders:read", "name": "Read Orders" },
    { "key": "orders:write", "name": "Write Orders" }
    ]
    }
    ],
    "featureFlags": [
    {
    "name": "New Checkout",
    "key": "new_checkout",
    "type": "bool",
    "allow_override_level": "env",
    "default_value": true
    }
    ],
    "envVars": [
    { "key": "PAYMENTS_GATEWAY", "value": "sandbox", "sensitive": false },
    { "key": "SECRET_PAYMENTS_GATEWAY", "value": "sandbox", "sensitive": true }
    ]
    }

3. Run the script

Link to this section
  1. Get the App keys from your Kinde M2M app. See the quick start guide for more information.
  1. Run the following code in your terminal to set your environment variables:

    Terminal window
    export KINDE_DOMAIN=<YOUR_KINDE_DOMAIN>.kinde.com
    export KINDE_CLIENT_ID=<KINDE_CLIENT_ID>
    export KINDE_CLIENT_SECRET=<KINDE_CLIENT_SECRET>
    export KINDE_AUDIENCE=https://<YOUR_KINDE_DOMAIN>.kinde.com/api
    export KINDE_SCOPES="create:applications create:application_redirect_uris create:application_logout_uris create:environment_variables create:feature_flags create:apis create:api_scopes create:roles create:permissions update:role_permissions"
    export CONFIG_PATH=./config/dev.json
  2. Run the script with the following terminal command. You should see a success message without any errors.

    Terminal window
    node scripts/seed-kinde.mjs

    Output:

    Terminal output
    Creating application...
    Creating environment variable...
    Creating environment variable...
    Creating API and Scopes...
    Creating roles and permissions...
    Creating auth redirect URLs...
    Created role: Admin
    Created API: Orders API
    Created environment variable PAYMENTS_GATEWAY
    Created environment variable SECRET_PAYMENTS_GATEWAY
    Created feature flag: New Checkout
    Creating auth logout URLs...
    Permission created: Read Orders
    Created a Kinde application with id <created_app_id>
    Permission created: Write Orders
    Added the permission "Read Orders" to the role Admin
    Added the permission "Write Orders" to the role Admin
    Kinde Seed Complete

If you prefer to use Terraform as Infrastructure as Code, there are two options you can consider:

Option 1: Terraform with Kinde community provider

Link to this section

There is a community Terraform provider for Kinde. It supports configuring the provider with domain and audience, and you can alias providers per environment. Treat it as community software, pin versions, and verify resource coverage in non-prod first.

  1. Enable the following additional scopes to your M2M application:

    • read:apis
    • update:apis
    • delete:apis
  2. Create the following files with the bash command:

    Terminal window
    touch provider.tf versions.tf variables.tf main.tf
  3. Enter the following code in versions.tf and save changes.

    terraform {
    required_providers {
    kinde = {
    source = "axatol/kinde"
    version = "0.0.1"
    }
    }
    }
  4. Enter the following code in provider.tf and save changes.

    provider "kinde" {
    domain = var.kinde_domain
    audience = var.kinde_audience
    client_id = var.kinde_client_id
    client_secret = var.kinde_client_secret
    }
  5. Enter the following code in variables.tf and save changes.

    variable kinde_domain {}
    variable kinde_audience {}
    variable kinde_client_id {}
    variable kinde_client_secret {}
  6. Enter the following code in main.tf and save changes.

    resource "kinde_api" "orders" {
    name = "Orders API"
    audience = "orders"
    }
  7. Initiate Terraform.

    Terminal window
    terraform init
  8. Create environment variables with the following bash command.

    Terminal window
    export TF_VAR_kinde_domain="https://<your_kinde_domain>.kinde.com"
    export TF_VAR_kinde_client_id="<your_client_id>"
    export TF_VAR_kinde_client_secret="<your_client_secret>"
    export TF_VAR_kinde_audience="https://<your_kinde_domain>.kinde.com/api"
  9. Run the Terraform project.

    Terminal window
    terraform apply

You will see a success message.

Terminal output
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# kinde_api.orders will be created
+ resource "kinde_api" "orders" {
+ audience = "orders"
+ id = (known after apply)
+ name = "Orders API"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
kinde_api.orders: Creating...
kinde_api.orders: Creation complete after 1s [id=the_id]

This will create an Orders API in your Kinde environment.

From here, add the resources that the provider currently supports and split values by workspace or tfvars files. Use Terraform workspaces or separate states to keep environments isolated.

If you pass providers down to modules, remember provider alias rules in Terraform.

Option 2: Terraform with seed script

Link to this section

If the provider’s resource coverage is not yet enough, you can still keep everything in Terraform by invoking the Node seed in local-exec. This preserves a single IaC entry point.

  1. Inside your original scripts directory, initialize Terraform with the command.

    Terminal window
    terraform init
  2. Create a new workspace for one of your environments (e.g. Staging).

    Terminal window
    terraform workspace new staging
  3. Create new Terraform files.

    touch main.tf variables.tf
  4. Open variables.tf with your favorite code editor, enter the following code and save changes.

    variable kinde_domain {}
    variable kinde_audience {}
    variable kinde_client_id {}
    variable kinde_client_secret {}
    variable kinde_scopes {}
  5. Open main.tf and enter the following code and save changes.

    locals {
    cfg_path = "./config/${terraform.workspace}.json"
    }
    resource "null_resource" "seed_kinde" {
    triggers = {
    cfg_sha = filesha256(local.cfg_path)
    }
    provisioner "local-exec" {
    command = "node ./scripts/seed-kinde.mjs"
    environment = {
    KINDE_ENV = terraform.workspace
    KINDE_DOMAIN = var.kinde_domain
    KINDE_AUDIENCE = var.kinde_audience
    KINDE_CLIENT_ID = var.kinde_client_id
    KINDE_CLIENT_SECRET= var.kinde_client_secret
    KINDE_SCOPES = var.kinde_scopes
    CONFIG_PATH = local.cfg_path
    }
    }
    }
  6. Create environment variables with the following bash command.

    Terminal window
    export TF_VAR_kinde_domain="<your_kinde_domain>.kinde.com"
    export TF_VAR_kinde_client_id="<your_client_id>"
    export TF_VAR_kinde_client_secret="<your_client_secret>"
    export TF_VAR_kinde_audience="https://<your_kinde_domain>.kinde.com/api"
    export TF_VAR_kinde_scopes="create:applications create:application_redirect_uris create:application_logout_uris create:environment_variables create:feature_flags create:apis create:api_scopes create:roles create:permissions update:role_permissions"
  7. Run the Terraform project:

    Terminal window
    terraform apply

You will see the following success output:

Terminal output
null_resource.seed_kinde: Creating...
null_resource.seed_kinde: Provisioning with 'local-exec'...
null_resource.seed_kinde (local-exec): Executing: ["/bin/sh" "-c" "node ./scripts/seed-kinde.mjs"]
null_resource.seed_kinde (local-exec): Creating application...
null_resource.seed_kinde (local-exec): Creating environment variable...
null_resource.seed_kinde (local-exec): Creating environment variable...
null_resource.seed_kinde (local-exec): Creating API and Scopes...
null_resource.seed_kinde (local-exec): Creating roles and permissions...
null_resource.seed_kinde (local-exec): Creating auth redirect URLs...
null_resource.seed_kinde (local-exec): Created API: Orders API
null_resource.seed_kinde (local-exec): Created environment variable SECRET_PAYMENTS_GATEWAY
null_resource.seed_kinde (local-exec): Created environment variable PAYMENTS_GATEWAY
null_resource.seed_kinde (local-exec): Created feature flag: New UI
null_resource.seed_kinde (local-exec): Creating auth logout URLs...
null_resource.seed_kinde (local-exec): Created role: Admin
null_resource.seed_kinde (local-exec): Created a Kinde application with id ***************
null_resource.seed_kinde (local-exec): Permission created: Read Orders
null_resource.seed_kinde (local-exec): Permission created: Write Orders
null_resource.seed_kinde (local-exec): Added the permission "Read Orders" to the role Admin
null_resource.seed_kinde (local-exec): Added the permission "Write Orders" to the role Admin
null_resource.seed_kinde (local-exec): Kinde Seed Complete
null_resource.seed_kinde: Creation complete after 3s [id=4289077431837605208]

Go to your Kinde dashboard to verify the changes.

GitHub workflow for CI automation

Link to this section

1. Create the workflow file

Link to this section
  1. Create a GitHub workflow directory and a workflow file seed-kinde.yml with the following bash command:

    mkdir -p .github/workflows
    touch .github/workflows/seed-kinde.yml
  2. Open seed-kinde.yml in your preferred code editor, add the following code, and save changes.

    name: Kinde Seed Automation
    on:
    workflow_dispatch:
    inputs:
    environment:
    description: "Which Kinde env to seed"
    required: true
    type: choice
    options: [dev, staging, prod]
    jobs:
    seed:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: "20"
    - run: node ./scripts/seed-kinde.mjs
    env:
    KINDE_ENV: ${{ inputs.environment }}
    KINDE_DOMAIN: ${{ secrets.KINDE_DOMAIN }} # e.g. subdomain.region.kinde.com
    KINDE_CLIENT_ID: ${{ secrets.KINDE_CLIENT_ID }}
    KINDE_CLIENT_SECRET: ${{ secrets.KINDE_CLIENT_SECRET }}
    KINDE_AUDIENCE: ${{ secrets.KINDE_AUDIENCE }} # management API audience
    # Keep scopes least-privilege. Add only what you need.
    KINDE_SCOPES: >-
    create:applications
    create:application_redirect_uris
    create:application_logout_uris
    create:environment_variables
    create:feature_flags
    create:apis create:api_scopes
    create:roles create:permissions
    update:role_permissions
    CONFIG_PATH: ./config/${{ inputs.environment }}.json
  3. Push the changes to GitHub.

    Terminal window
    git add .
    git commit -m "Add Kinde Seed Automation workflow"
    git push

2. Create environment variables in GitHub

Link to this section
  1. Open your GitHub repository and go to Settings > Secrets and variables > Actions > Repository secrets > select New repository secret.

  2. Add all the following environment variable values and select Add secret.

    KINDE_DOMAIN
    KINDE_CLIENT_ID
    KINDE_CLIENT_SECRET
    KINDE_AUDIENCE
  3. Go to Actions > Kinde Seed Automation > select Run workflow.

  4. In the drop-down menu:

    1. Select the Kinde environment to seed (e.g., staging), select Run workflow.
    2. Wait for the workflow to complete.
  5. Select the workflow after it finishes running.

  6. Go to seed > select Run node ./scripts/seed-kinde.mjs to expand the logs. You will see the seed output logs.

Now you can automate the Kinde seed script with your CI pipeline.

Verify in Kinde dashboard

Link to this section
  1. Go to your Kinde dashboard and switch to the correct environment.
  2. You will see that your automations created the following:

A new application in your dashboard with the configured redirect and logout URLs.

kinde new application

Environment variables

Link to this section

environment variables

feature flags

APIs and scopes

Link to this section

apis and scopes

Roles and permissions

Link to this section

Roles created with permissions assigned.

permissions

Run the same script for all of your environments and config presets as required.

What are the current limitations?

Link to this section
  • No cross-env copy today. Replicate settings using the seed script. Plan for drift detection by re-running the seed script on each deploy.
  • Least privilege: Use least-privilege scopes on your M2M app that runs automation.

What should go under configuration as code?

Link to this section
  • Applications and their callbacks.
  • APIs and scopes that your apps use.
  • Roles and permissions, feature flags, and environment variables with safe defaults for each environment.

How do I create additional Kinde environments?

Link to this section
  1. Select the environment drop-down in the top left of the Kinde home screen, and select All environments.

    add new environment menu in kinde

  2. Select Add environment. A dialog opens.

    1. Enter a Name for the environment (e.g., Development)
    2. Enter a short Code to reference this environment in code (e.g., dev).
    3. Select Save.
  3. Create any additional environments you need (e.g. Staging, Testing).