> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 4. Add a database and wire its secret

> Store the database credentials as a secret, install PostgreSQL from the Template Catalog in its own GVC, and deliver the same secret to the API through an identity, a policy, and a reference.

## Overview

The API keeps the waitlist in memory, so every [location](/concepts/location) has its own list and a restart empties it. This part stores the database credentials in a secret, installs a PostgreSQL database from the [Template Catalog](/template-catalog/overview) in its own GVC, and points the API at it, so a stateless tier that spans providers writes to one source of truth. The database and the API read that secret the way every [workload](/concepts/workload) on Control Plane reads every secret, through an [identity](/concepts/identity), a [policy](/concepts/access-control), and a reference, so the password appears in no manifest or environment variable.

**What you'll build:**

* A dictionary secret, `db-credentials`, holding the database user, password, and name.
* A second [GVC](/concepts/gvc), `quickstart-db`, with a single location.
* A PostgreSQL release named `db` in it: the `db-postgres` workload, its [volume set](/reference/volumeset), and the identity and policy the template creates so the database can read the secret.
* An identity for the API, `api-identity`, and a policy granting it `reveal` on the same secret.
* The `api` workload writing every location's signups to the one database.

<img src="https://mintcdn.com/controlplanecorporation-majid-docs-content-expansion/6V8SaiZLWK_iHnrB/images/quickstart/database-and-secret.svg?fit=max&auto=format&n=6V8SaiZLWK_iHnrB&q=85&s=b20f03ff6e1448591149c48dad1ead82" alt="The workload api connects to db-postgres at db-postgres.quickstart-db.cpln.local:5432 across GVCs over the internal network. The API's identity api-identity and the database's identity db-pg-identity both reveal the secret db-credentials, each granted by its own policy. One secret, two readers, and one database that every location writes to." style={{maxWidth:'720px',width:'100%',margin:'1.75rem auto',display:'block'}} width="720" height="358" data-path="images/quickstart/database-and-secret.svg" />

<AccordionGroup>
  <Accordion title="Why a second GVC">
    This template runs one PostgreSQL [replica](/concepts/replica) with one volume. In `quickstart-gvc` it would run once per location, as two separate databases, so it gets a GVC with a single location; a database that spans locations is the [Postgres Multi-Location](/template-catalog/templates/postgres-multi-location) template. The API reaches it across GVCs over the internal network, admitted by the release's [internal firewall](/reference/workload/firewall#internal).
  </Accordion>

  <Accordion title="How secret access works">
    A workload reads a [secret](/reference/secret) only when three things are all in place:

    | Step         | What must be true                                                                                                         | Without it                                                                                                                          |
    | ------------ | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
    | 1. Identity  | An identity is attached to the workload (`spec.identityLink`)                                                             | The workload has nothing to authenticate as; its secret references cannot resolve and its [deployment](/concepts/deployment) pauses |
    | 2. Policy    | A policy grants that identity `reveal` on the secret                                                                      | The identity is not allowed to read the secret; the deployment stays paused until access is granted                                 |
    | 3. Reference | The secret is [injected](/reference/workload/containers#secret-variables) as `cpln://secret/NAME.KEY` (env var or volume) | The workload never receives the value; there is nothing to inject                                                                   |

    The permission to grant is `reveal`. The similarly named `view` permission exposes only a secret's metadata, never its value.
  </Accordion>
</AccordionGroup>

## Prerequisites

* Completed [3. Service-to-service communication](/quickstart/connect-workloads), with `api` storing the frontend's signups in memory.
* For the CLI and AI Agent paths: the CLI installed and logged in, as in part 2.

<Tabs>
  <Tab title="Console" icon="display">
    ## Step 1: Store the database credentials as a secret

    <Steps>
      <Step title="Open the secret form">
        Click `Secrets` in the left menu, then click `New`. Enter `db-credentials` as the `Name` and select `Dictionary` as the `Type`.
      </Step>

      <Step title="Add the three entries">
        Click `Data` in the left pane and add three rows, each a `Key` and a `Value`:

        | Key        | Value                  |
        | ---------- | ---------------------- |
        | `username` | `api`                  |
        | `password` | a password of your own |
        | `database` | `waitlist`             |

        Click `Create`. PostgreSQL creates that user and that database on its first boot, and the API signs in with the same values.
      </Step>
    </Steps>

    ## Step 2: Create the database GVC

    <Steps>
      <Step title="Open the Create GVC form">
        Click the `Create` dropdown in the upper right corner and select `GVC`.
      </Step>

      <Step title="Name it and pick one location">
        Enter `quickstart-db` as the name. Click `Locations`, then `Add Location`, select `aws-us-west-2`, and click `OK`.
      </Step>

      <Step title="Create the GVC">
        Click `Create`. When the Console asks `Set as Current Context?`, click `No, Just View`, so the sidebar keeps showing `quickstart-gvc`.
      </Step>
    </Steps>

    ## Step 3: Install PostgreSQL from the Template Catalog

    <Steps>
      <Step title="Open the template">
        In the left menu under `Templates`, click `Catalog`, then select the `postgres` template and click `Create Template Release`.
      </Step>

      <Step title="Configure the release">
        Select `quickstart-db` as the `GVC`, enter `db` as the `Release Name`, and make sure `3.4.1` is the selected `Template Version`. This part is written against 3.4.1; other versions differ.
      </Step>

      <Step title="Point it at the secret and admit the API">
        In the values editor, under `config`, set `credentialsSecretName` to `db-credentials`. Under `internalAccess`, set `type` to `workload-list` and list `//gvc/quickstart-gvc/workload/api` under `workloads`, so the API is admitted from its own GVC.
      </Step>

      <Step title="Install">
        Click `Install App`. The Console opens the new release, listing what it created: the `db-postgres` workload, the `db-pg-vs` volume set, and the `db-pg-identity` identity with the `db-pg-policy` policy that lets the database read your secret.
      </Step>
    </Steps>

    ## Step 4: Create an identity for the API

    With `quickstart-gvc` as the current context, click `Identities` in the left menu, then click `New`. Enter `api-identity` as the `Name` and click `Create`.

    ## Step 5: Grant access with a policy

    <Steps>
      <Step title="Create the policy">
        Click `Policies` in the left menu, then click `New`. Enter `api-db-policy` as the `Name`. Click `Target` in the left pane and select `Secret` as the `Kind`.
      </Step>

      <Step title="Target the secret">
        Click `Items` in the left pane, add an item, select `db-credentials`, and confirm.
      </Step>

      <Step title="Bind the reveal permission">
        Click `Bindings` in the left pane and add a binding. Under `Permissions` select `reveal`; under `Identities` select `api-identity` from the `quickstart-gvc` GVC. Confirm.
      </Step>

      <Step title="Create the policy">
        Click `Create`.
      </Step>
    </Steps>

    ## Step 6: Attach the identity and reference the secret

    <Steps>
      <Step title="Attach the identity">
        Open the `api` workload in `quickstart-gvc`, click `Identity` in the left pane, and select `api-identity` under `Identity Name`.
      </Step>

      <Step title="Add the connection variables">
        Click `Containers` in the left pane and open the `Env Vars` tab. Click `Add Environment Variable` four times and fill the rows:

        | Name         | Value type      | Value                                               |
        | ------------ | --------------- | --------------------------------------------------- |
        | `PGHOST`     | `Literal Value` | `db-postgres.quickstart-db.cpln.local`              |
        | `PGDATABASE` | `CPLN Secret`   | `db-credentials`, with `database` as the `Property` |
        | `PGUSER`     | `CPLN Secret`   | `db-credentials`, with `username` as the `Property` |
        | `PGPASSWORD` | `CPLN Secret`   | `db-credentials`, with `password` as the `Property` |

        The three secret rows become the references `cpln://secret/db-credentials.database`, `cpln://secret/db-credentials.username`, and `cpln://secret/db-credentials.password`.
      </Step>

      <Step title="Update">
        Click `Update`. The workload rolls out a new version whose container receives the credentials from the secret.
      </Step>
    </Steps>
  </Tab>

  <Tab title="CLI" icon="terminal">
    ## Step 1: Store the database credentials as a secret

    The password is generated and never printed; PostgreSQL creates that user and that database on its first boot, and the API signs in with the same values:

    ```bash theme={null}
    cpln secret create-dictionary --name db-credentials \
      --entry username=api \
      --entry password="$(openssl rand -hex 16)" \
      --entry database=waitlist
    ```

    ## Step 2: Create the database GVC

    ```bash theme={null}
    cpln gvc create --name quickstart-db --location aws-us-west-2
    ```

    ## Step 3: Install PostgreSQL from the Template Catalog

    Create `db-values.yaml`. It points the release at the secret and admits `api` from its own GVC:

    ```yaml theme={null}
    config:
      credentialsSecretName: db-credentials
    internalAccess:
      type: workload-list
      workloads:
        - //gvc/quickstart-gvc/workload/api
    ```

    Install the template as a release named `db`:

    ```bash theme={null}
    cpln helm install db oci://ghcr.io/controlplane-com/templates/postgres \
      --version 3.4.1 \
      --gvc quickstart-db \
      -f db-values.yaml
    ```

    The output lists what the release created:

    ```text theme={null}
    Created /org/my-org/gvc/quickstart-db/identity/db-pg-identity
    Created /org/my-org/gvc/quickstart-db/volumeset/db-pg-vs
    Created /org/my-org/policy/db-pg-policy
    Created /org/my-org/gvc/quickstart-db/workload/db-postgres

    Release 'db' has been installed successfully!
    ```

    The `db-pg-policy` policy lets the database's own identity read your secret; the API gets the same access in the next steps.

    ## Step 4: Create an identity for the API

    ```bash theme={null}
    cpln identity create --name api-identity --gvc quickstart-gvc
    ```

    ## Step 5: Grant access with a policy

    Create a policy targeting the secret:

    ```bash theme={null}
    cpln policy create --name api-db-policy \
      --target-kind secret \
      --resource db-credentials
    ```

    Bind the `reveal` permission to the identity:

    ```bash theme={null}
    cpln policy add-binding api-db-policy \
      --permission reveal \
      --identity api-identity \
      --gvc quickstart-gvc
    ```

    ## Step 6: Attach the identity and reference the secret

    ```bash theme={null}
    cpln workload update api --gvc quickstart-gvc \
      --set spec.identityLink=api-identity \
      --set spec.containers.api.env.PGHOST.value=db-postgres.quickstart-db.cpln.local \
      --set spec.containers.api.env.PGDATABASE.value=cpln://secret/db-credentials.database \
      --set spec.containers.api.env.PGUSER.value=cpln://secret/db-credentials.username \
      --set spec.containers.api.env.PGPASSWORD.value=cpln://secret/db-credentials.password
    ```

    The workload rolls out a new version whose container receives the credentials from the secret.
  </Tab>

  <Tab title="Terraform" icon="https://mintcdn.com/controlplanecorporation-majid-docs-content-expansion/Ry1Mkgc7uPHC-gur/icons/terraform.svg?fit=max&auto=format&n=Ry1Mkgc7uPHC-gur&q=85&s=19deabd5e978d39905a6c83ea1f7904d" width="256" height="291" data-path="icons/terraform.svg">
    ## Step 1: Define the secret, the database GVC, and the release

    Add to your `main.tf`. The password arrives through a sensitive variable, so it lands in no configuration file; the release depends on the secret because PostgreSQL reads it on its first boot:

    ```hcl theme={null}
    variable "db_password" {
      type      = string
      sensitive = true
    }

    resource "cpln_secret" "db_credentials" {
      name = "db-credentials"

      dictionary = {
        username = "api"
        password = var.db_password
        database = "waitlist"
      }
    }

    resource "cpln_gvc" "db" {
      name        = "quickstart-db"
      description = "The database GVC"

      locations = ["aws-us-west-2"]
    }

    resource "cpln_catalog_template" "db" {
      depends_on = [cpln_secret.db_credentials]

      name     = "db"
      template = "postgres"
      version  = "3.4.1"
      gvc      = cpln_gvc.db.name

      values = <<-EOT
        config:
          credentialsSecretName: db-credentials
        internalAccess:
          type: workload-list
          workloads:
            - //gvc/quickstart-gvc/workload/api
      EOT
    }
    ```

    The release creates the `db-postgres` workload, its volume set, and the identity and policy that let the database read the secret; every other value keeps the template's default.

    ## Step 2: Define the identity and the policy

    ```hcl theme={null}
    resource "cpln_identity" "api" {
      gvc  = cpln_gvc.quickstart.name
      name = "api-identity"
    }

    resource "cpln_policy" "api_db" {
      name         = "api-db-policy"
      target_kind  = "secret"
      target_links = [cpln_secret.db_credentials.name]

      binding {
        permissions     = ["reveal"]
        principal_links = [cpln_identity.api.self_link]
      }
    }
    ```

    ## Step 3: Attach the identity and reference the secret

    In `cpln_workload.api`, attach the identity, add the connection variables to the container, and make the workload depend on the policy so the references resolve on its first rollout:

    ```hcl theme={null}
    resource "cpln_workload" "api" {
      depends_on = [cpln_policy.api_db]

      identity_link = cpln_identity.api.self_link

      # ...

        env = {
          PGHOST     = "db-postgres.quickstart-db.cpln.local"
          PGDATABASE = "cpln://secret/db-credentials.database"
          PGUSER     = "cpln://secret/db-credentials.username"
          PGPASSWORD = "cpln://secret/db-credentials.password"
        }
    ```

    ## Step 4: Apply

    `var.db_password` has no default, so write it once to a file Terraform reads on every run:

    ```bash theme={null}
    printf 'db_password = "%s"\n' "$(openssl rand -hex 16)" > terraform.tfvars
    ```

    Then apply:

    ```bash theme={null}
    terraform apply
    ```

    <Warning>
      Keep `terraform.tfvars` out of version control, and leave the password alone once the database has booted. PostgreSQL creates the user on its first boot and keeps it on its volume, so changing the secret afterwards leaves the API unable to sign in the next time its replicas restart, including the forced redeployment in [part 5](/quickstart/observe-workload). Without the file, Terraform prompts for the password on every later apply and on `terraform destroy`, and a different answer rotates it.
    </Warning>

    <Tip>
      The [Terraform Registry documentation](https://registry.terraform.io/providers/controlplane-com/cpln/latest/docs/resources/catalog_template) lists every catalog template option.
    </Tip>
  </Tab>

  <Tab title="Pulumi" icon="https://mintcdn.com/controlplanecorporation-majid-docs-content-expansion/Ry1Mkgc7uPHC-gur/icons/pulumi.svg?fit=max&auto=format&n=Ry1Mkgc7uPHC-gur&q=85&s=7a7f4b9390dfa8fecf6223c88c658dcd" width="256" height="271" data-path="icons/pulumi.svg">
    ## Step 1: Store the database password

    ```bash theme={null}
    pulumi config set --secret dbPassword $(openssl rand -hex 16)
    ```

    ## Step 2: Define the secret, the database GVC, and the release

    The release depends on the secret because PostgreSQL reads it on its first boot; every value other than the secret name and the access keeps the template's default.

    <Warning>
      Add these resources, and the identity and policy in step 3, above the `api` workload you wrote in part 3, not below it. Step 4 makes `api` reference the identity and the policy, and every language on this page resolves names in the order they appear: a reference that sits above its declaration fails to compile in TypeScript, Go, and C#, and raises `NameError` in Python.
    </Warning>

    <Tabs>
      <Tab title="TypeScript">
        Add to your `index.ts`:

        ```typescript theme={null}
        import * as pulumi from "@pulumi/pulumi";

        const config = new pulumi.Config();
        const dbPassword = config.requireSecret("dbPassword");

        // The database credentials, read by PostgreSQL and by the API
        const dbCredentials = new cpln.Secret("db-credentials", {
          name: "db-credentials",
          dictionary: {
            username: "api",
            password: dbPassword,
            database: "waitlist",
          },
        });

        // The database GVC: one location, because PostgreSQL keeps its data on a disk
        const dbGvc = new cpln.Gvc("quickstart-db", {
          name: "quickstart-db",
          description: "The database GVC",
          locations: ["aws-us-west-2"],
        });

        // PostgreSQL from the Template Catalog
        const db = new cpln.CatalogTemplate("db", {
          name: "db",
          template: "postgres",
          version: "3.4.1",
          gvc: dbGvc.name,
          values: `config:
          credentialsSecretName: db-credentials
        internalAccess:
          type: workload-list
          workloads:
            - //gvc/quickstart-gvc/workload/api
        `,
        }, { dependsOn: [dbCredentials] });
        ```
      </Tab>

      <Tab title="Python">
        Add to your `__main__.py`:

        ```python theme={null}
        config = pulumi.Config()
        db_password = config.require_secret("dbPassword")

        # The database credentials, read by PostgreSQL and by the API
        db_credentials = cpln.Secret("db-credentials",
            name="db-credentials",
            dictionary={
                "username": "api",
                "password": db_password,
                "database": "waitlist",
            })

        # The database GVC: one location, because PostgreSQL keeps its data on a disk
        db_gvc = cpln.Gvc("quickstart-db",
            name="quickstart-db",
            description="The database GVC",
            locations=["aws-us-west-2"])

        # PostgreSQL from the Template Catalog
        db = cpln.CatalogTemplate("db",
            name="db",
            template="postgres",
            version="3.4.1",
            gvc=db_gvc.name,
            values="""config:
          credentialsSecretName: db-credentials
        internalAccess:
          type: workload-list
          workloads:
            - //gvc/quickstart-gvc/workload/api
        """,
            opts=pulumi.ResourceOptions(depends_on=[db_credentials]))
        ```
      </Tab>

      <Tab title="Go">
        Add `github.com/pulumi/pulumi/sdk/v3/go/pulumi/config` to your imports and run `go mod tidy`, which records the dependency that package pulls in. Then add inside `pulumi.Run` in your `main.go`:

        ```go theme={null}
        conf := config.New(ctx, "")
        dbPassword := conf.RequireSecret("dbPassword")

        // The database credentials, read by PostgreSQL and by the API
        dbCredentials, err := cpln.NewSecret(ctx, "db-credentials", &cpln.SecretArgs{
        	Name: pulumi.String("db-credentials"),
        	Dictionary: pulumi.StringMap{
        		"username": pulumi.String("api"),
        		"password": dbPassword,
        		"database": pulumi.String("waitlist"),
        	},
        })
        if err != nil {
        	return err
        }

        // The database GVC: one location, because PostgreSQL keeps its data on a disk
        dbGvc, err := cpln.NewGvc(ctx, "quickstart-db", &cpln.GvcArgs{
        	Name:        pulumi.String("quickstart-db"),
        	Description: pulumi.String("The database GVC"),
        	Locations:   pulumi.StringArray{pulumi.String("aws-us-west-2")},
        })
        if err != nil {
        	return err
        }

        // PostgreSQL from the Template Catalog
        db, err := cpln.NewCatalogTemplate(ctx, "db", &cpln.CatalogTemplateArgs{
        	Name:     pulumi.String("db"),
        	Template: pulumi.String("postgres"),
        	Version:  pulumi.String("3.4.1"),
        	Gvc:      dbGvc.Name,
        	Values: pulumi.String(`config:
          credentialsSecretName: db-credentials
        internalAccess:
          type: workload-list
          workloads:
            - //gvc/quickstart-gvc/workload/api
        `),
        }, pulumi.DependsOn([]pulumi.Resource{dbCredentials}))
        if err != nil {
        	return err
        }
        _ = db
        ```
      </Tab>

      <Tab title="C#">
        Add to your `Program.cs`:

        ```csharp theme={null}
        var config = new Pulumi.Config();
        var dbPassword = config.RequireSecret("dbPassword");

        // The database credentials, read by PostgreSQL and by the API
        var dbCredentials = new Secret("db-credentials", new SecretArgs
        {
            Name = "db-credentials",
            Dictionary =
            {
                { "username", "api" },
                { "password", dbPassword },
                { "database", "waitlist" }
            }
        });

        // The database GVC: one location, because PostgreSQL keeps its data on a disk
        var dbGvc = new Gvc("quickstart-db", new GvcArgs
        {
            Name = "quickstart-db",
            Description = "The database GVC",
            Locations = new[] { "aws-us-west-2" }
        });

        // PostgreSQL from the Template Catalog
        var db = new CatalogTemplate("db", new CatalogTemplateArgs
        {
            Name = "db",
            Template = "postgres",
            Version = "3.4.1",
            Gvc = dbGvc.Name,
            Values = @"config:
          credentialsSecretName: db-credentials
        internalAccess:
          type: workload-list
          workloads:
            - //gvc/quickstart-gvc/workload/api
        "
        }, new CustomResourceOptions { DependsOn = { dbCredentials } });
        ```
      </Tab>
    </Tabs>

    The release creates the `db-postgres` workload, its volume set, and the identity and policy that let the database read the secret.

    ## Step 3: Define the identity and the policy

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        // Identity for the API
        const apiIdentity = new cpln.Identity("api-identity", {
          gvc: gvc.name,
          name: "api-identity",
        });

        // Policy granting the identity reveal on the database credentials
        const apiDbPolicy = new cpln.Policy("api-db-policy", {
          name: "api-db-policy",
          targetKind: "secret",
          targetLinks: [dbCredentials.name],
          bindings: [
            {
              permissions: ["reveal"],
              principalLinks: [apiIdentity.selfLink],
            },
          ],
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        # Identity for the API
        api_identity = cpln.Identity("api-identity",
            gvc=gvc.name,
            name="api-identity")

        # Policy granting the identity reveal on the database credentials
        api_db_policy = cpln.Policy("api-db-policy",
            name="api-db-policy",
            target_kind="secret",
            target_links=[db_credentials.name],
            bindings=[cpln.PolicyBindingArgs(
                permissions=["reveal"],
                principal_links=[api_identity.self_link],
            )])
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        // Identity for the API
        apiIdentity, err := cpln.NewIdentity(ctx, "api-identity", &cpln.IdentityArgs{
        	Gvc:  gvc.Name,
        	Name: pulumi.String("api-identity"),
        })
        if err != nil {
        	return err
        }

        // Policy granting the identity reveal on the database credentials
        apiDbPolicy, err := cpln.NewPolicy(ctx, "api-db-policy", &cpln.PolicyArgs{
        	Name:        pulumi.String("api-db-policy"),
        	TargetKind:  pulumi.String("secret"),
        	TargetLinks: pulumi.StringArray{dbCredentials.Name},
        	Bindings: cpln.PolicyBindingArray{
        		&cpln.PolicyBindingArgs{
        			Permissions:    pulumi.StringArray{pulumi.String("reveal")},
        			PrincipalLinks: pulumi.StringArray{apiIdentity.SelfLink},
        		},
        	},
        })
        if err != nil {
        	return err
        }
        ```
      </Tab>

      <Tab title="C#">
        ```csharp theme={null}
        // Identity for the API
        var apiIdentity = new Identity("api-identity", new IdentityArgs
        {
            Gvc = gvc.Name,
            Name = "api-identity"
        });

        // Policy granting the identity reveal on the database credentials
        var apiDbPolicy = new Policy("api-db-policy", new PolicyArgs
        {
            Name = "api-db-policy",
            TargetKind = "secret",
            TargetLinks = { dbCredentials.Name },
            Bindings = new[]
            {
                new PolicyBindingArgs
                {
                    Permissions = new[] { "reveal" },
                    PrincipalLinks = { apiIdentity.SelfLink }
                }
            }
        });
        ```
      </Tab>
    </Tabs>

    ## Step 4: Attach the identity and reference the secret

    In the `api` workload, attach the identity, add the connection variables to the container, and make the workload depend on the policy so the references resolve on its first rollout:

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
          identityLink: apiIdentity.selfLink,
          // ...
              env: {
                PGHOST: "db-postgres.quickstart-db.cpln.local",
                PGDATABASE: "cpln://secret/db-credentials.database",
                PGUSER: "cpln://secret/db-credentials.username",
                PGPASSWORD: "cpln://secret/db-credentials.password",
              },
          // ...
        }, { dependsOn: [apiDbPolicy] });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
            identity_link=api_identity.self_link,
            # ...
                env={
                    "PGHOST": "db-postgres.quickstart-db.cpln.local",
                    "PGDATABASE": "cpln://secret/db-credentials.database",
                    "PGUSER": "cpln://secret/db-credentials.username",
                    "PGPASSWORD": "cpln://secret/db-credentials.password",
                },
            # ...
            opts=pulumi.ResourceOptions(depends_on=[api_db_policy]))
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        	IdentityLink: apiIdentity.SelfLink,
        	// ...
        			Env: pulumi.StringMap{
        				"PGHOST":     pulumi.String("db-postgres.quickstart-db.cpln.local"),
        				"PGDATABASE": pulumi.String("cpln://secret/db-credentials.database"),
        				"PGUSER":     pulumi.String("cpln://secret/db-credentials.username"),
        				"PGPASSWORD": pulumi.String("cpln://secret/db-credentials.password"),
        			},
        	// ...
        }, pulumi.DependsOn([]pulumi.Resource{apiDbPolicy}))
        ```
      </Tab>

      <Tab title="C#">
        ```csharp theme={null}
            IdentityLink = apiIdentity.SelfLink,
            // ...
                    Env =
                    {
                        { "PGHOST", "db-postgres.quickstart-db.cpln.local" },
                        { "PGDATABASE", "cpln://secret/db-credentials.database" },
                        { "PGUSER", "cpln://secret/db-credentials.username" },
                        { "PGPASSWORD", "cpln://secret/db-credentials.password" }
                    }
            // ...
        }, new CustomResourceOptions { DependsOn = { apiDbPolicy } });
        ```
      </Tab>
    </Tabs>

    ## Step 5: Deploy

    ```bash theme={null}
    pulumi up
    ```

    <Tip>
      The [Pulumi Registry documentation](https://www.pulumi.com/registry/packages/cpln/api-docs/catalogtemplate/) lists every catalog template option.
    </Tip>
  </Tab>

  <Tab title="AI Agent" icon="sparkles">
    ## Step 1: Store the database credentials as a secret

    The agent never handles secret values, so create the secret yourself. With the CLI, the password is generated and never printed:

    ```bash theme={null}
    cpln secret create-dictionary --name db-credentials \
      --entry username=api \
      --entry password="$(openssl rand -hex 16)" \
      --entry database=waitlist
    ```

    PostgreSQL creates that user and that database on its first boot, and the API signs in with the same values.

    ## Step 2: Create the database GVC

    The first prompt names your org, so it works in a new conversation too. Replace `my-org` with your org name:

    ```text theme={null}
    Using org "my-org", create a GVC called "quickstart-db" with the
    single location aws-us-west-2.
    ```

    The agent creates the GVC with its one location.

    ## Step 3: Install PostgreSQL from the Template Catalog

    ```text theme={null}
    Install the postgres template, version 3.4.1, in that GVC as a
    release called "db". Use the secret "db-credentials" for its
    credentials, and let only the api workload in "quickstart-gvc"
    reach it.
    ```

    The agent reads the template's example values, sets the secret name and the workload list in them, and installs the release. The release creates the `db-postgres` workload, the `db-pg-vs` volume set, and the `db-pg-identity` identity with the `db-pg-policy` policy that lets the database read your secret.

    ## Step 4: Grant the API access to the secret

    ```text theme={null}
    Give the api workload access to the secret "db-credentials",
    through an identity called "api-identity" and a policy called
    "api-db-policy".
    ```

    The agent creates the identity in `quickstart-gvc`, attaches it to `api`, and creates the policy granting the identity `reveal` on the secret, in one call. The secret's value never reaches the agent.

    ## Step 5: Reference the secret

    ```text theme={null}
    Set these environment variables on the api workload: PGHOST to
    db-postgres.quickstart-db.cpln.local, and PGDATABASE, PGUSER,
    and PGPASSWORD to the database, username, and password entries
    of the secret "db-credentials".
    ```

    The agent adds the four variables, the last three as `cpln://secret/db-credentials.database`, `.username`, and `.password` references, and follows the rollout. The new version's container receives the credentials from the secret.
  </Tab>
</Tabs>

## Verify

Once `db-postgres` and the new `api` version report `Ready`, reload the frontend. The badge next to the `Recent signups` heading now says `Stored in PostgreSQL` with the location nearest to you, and the list is empty because the part 3 signups lived in memory. Join the waitlist, then open another location's endpoint from the `Deployments` page (or ask your AI agent for it). It shows the same list, because every API replica now writes to the one database in `quickstart-db`.

The list also survives a restart. Force a redeployment of `api` (`Actions`, then `Force Redeployment` in the Console, or `cpln workload force-redeployment api --gvc quickstart-gvc`), reload once it reports `Ready` (a `503` from the API means the mesh is still switching, so reload once more), and the signups are still there.

<Check>
  The API authenticated to a database it reaches across GVCs, with credentials it reads through the identity, policy, and secret reference chain, and the password appears in no manifest, configuration, or image.
</Check>

<Note>
  A workload that references a secret its identity is not allowed to reveal does not roll out. The deployment reports `The identity api-identity is not allowed to reveal the secret db-credentials. Workload updates are paused until the identity is granted access or the reference to the secret is removed.`, while the version already serving keeps answering. Grant the access, then resume with `cpln workload force-redeployment api --gvc quickstart-gvc` or, in the Console, `Actions` then `Force Redeployment`.
</Note>

## What you've learned

* **One secret, two readers**: the database and the API read the same dictionary secret, each through its own identity and policy, and the value sits in neither workload's configuration.
* **A template is a whole service**: one catalog release brings the workload, its volume set, and the identity and policy it needs, managed together as the release.
* **Unreplicated state stays in one location**: this template runs one PostgreSQL replica with one volume, so it lives in a single-location GVC. A production database uses a replicated template such as Postgres Multi-Location, which spans locations with a primary, streaming replicas, and automatic failover.
* **The access chain**: an identity attached to the workload, a policy granting it `reveal` on the secret, and a `cpln://secret/` reference where the value is needed. Miss one and the value is absent.
* **`reveal`, not `view`**: `view` exposes a secret's metadata, only `reveal` exposes its value.
* **Safe by default**: a workload whose reference cannot resolve keeps serving its previous version instead of rolling out without the value.

## Next steps

<Card title="5. Observe your workload" icon="chart-line" href="/quickstart/observe-workload" horizontal>
  Follow one request through the logs of both workloads, read the metrics every workload publishes, and open a trace that spans the frontend and the API.
</Card>

## Clean up

To remove everything the series has created so far:

<Tabs>
  <Tab title="Console" icon="display">
    <Steps>
      <Step title="Delete the policy">
        Open `Policies`, select `api-db-policy`, click `Actions`, then `Delete`, and confirm.
      </Step>

      <Step title="Uninstall the database release">
        Under `Templates`, click `Releases`, open `db`, click `Actions`, then `Uninstall`, and confirm.
      </Step>

      <Step title="Delete the database GVC">
        Open `quickstart-db`, click `Actions`, then `Delete`, type the GVC name to confirm, and click `Delete`.
      </Step>

      <Step title="Delete the application GVC">
        Open `quickstart-gvc`, click `Actions`, then `Delete`, type the GVC name to confirm, and click `Delete`. `web`, `frontend`, `api`, and `api-identity` go with it.
      </Step>

      <Step title="Delete the secret">
        Open `Secrets`, select `db-credentials`, click `Actions`, then `Delete`, and confirm.
      </Step>

      <Step title="Delete the images">
        Open `Images`, select `frontend`, and click `Actions`, then `Delete` to remove all its tags. Repeat for `api`.
      </Step>
    </Steps>
  </Tab>

  <Tab title="CLI" icon="terminal">
    ```bash theme={null}
    cpln policy delete api-db-policy
    cpln helm uninstall db
    cpln gvc delete quickstart-db
    cpln gvc delete quickstart-gvc
    cpln secret delete db-credentials
    cpln image delete frontend:1.0
    cpln image delete frontend:1.1
    cpln image delete api:1.0
    ```
  </Tab>

  <Tab title="Terraform" icon="https://mintcdn.com/controlplanecorporation-majid-docs-content-expansion/Ry1Mkgc7uPHC-gur/icons/terraform.svg?fit=max&auto=format&n=Ry1Mkgc7uPHC-gur&q=85&s=19deabd5e978d39905a6c83ea1f7904d" width="256" height="291" data-path="icons/terraform.svg">
    ```bash theme={null}
    terraform destroy
    ```

    The images were built by the CLI, so delete them with it: `cpln image delete frontend:1.0`, `cpln image delete frontend:1.1`, and `cpln image delete api:1.0`.
  </Tab>

  <Tab title="Pulumi" icon="https://mintcdn.com/controlplanecorporation-majid-docs-content-expansion/Ry1Mkgc7uPHC-gur/icons/pulumi.svg?fit=max&auto=format&n=Ry1Mkgc7uPHC-gur&q=85&s=7a7f4b9390dfa8fecf6223c88c658dcd" width="256" height="271" data-path="icons/pulumi.svg">
    ```bash theme={null}
    pulumi destroy
    ```

    The images were built by the CLI, so delete them with it: `cpln image delete frontend:1.0`, `cpln image delete frontend:1.1`, and `cpln image delete api:1.0`.
  </Tab>

  <Tab title="AI Agent" icon="sparkles">
    ```text theme={null}
    Delete the policy "api-db-policy", uninstall the release "db",
    then delete the GVCs "quickstart-db" and "quickstart-gvc" and
    the images frontend:1.0, frontend:1.1, and api:1.0.
    ```

    The agent lists what goes, including the release's volume set and the data on it, and asks you to confirm. The agent cannot delete a secret, so remove that yourself:

    ```bash theme={null}
    cpln secret delete db-credentials
    ```
  </Tab>
</Tabs>

<Note>
  Uninstalling the release removes its volume set and the data on it. A `--remote` build also pushes the build cache images `frontend-cache:latest` and `api-cache:latest`; delete those too if you built without Docker.
</Note>
