Projects and environments
Secrets live in an environment. Environments belong to a project. That is the whole hierarchy.
project "api"├── environment "production"│ ├── DATABASE_URL│ └── STRIPE_SECRET_KEY└── environment "staging" ├── DATABASE_URL └── STRIPE_SECRET_KEYGrants are scoped to a project or to one of its environments, so the same hierarchy that organises your secrets is what controls access to them. See Access control.
Before you begin
Every command here needs an authenticated machine. Start with Authentication.
Slugs and names
Every project and environment has two identifiers:
| Field | What it is |
|---|---|
| Slug | The URL-safe identifier that commands, URLs and scopes use |
| Name | Free text, for people to read |
eu-west is a slug; “EU West” is a name. Commands take the slug.
prk env create "EU West" --slug eu-west --project apiLeave --slug off and it is derived from the name. A slug you pass explicitly
is validated rather than mangled, so a typo is reported instead of silently
becoming a different identifier.
| Field | Rule |
|---|---|
| Slug | Lowercase letters and digits, single interior hyphens. No leading or trailing hyphen. 1–64 characters |
| Name | 1–128 characters, free text |
| Description | Up to 1024 characters, or null |
Project slugs are unique across the server; environment slugs are unique within their project.
The slug grammar excludes / and : on purpose. That is what lets the alias
routes /p/:slug/e/:slug/… and the project:environment scope syntax both
parse with no escaping.
Even so, the scope parser splits on the first colon only, so a:b:c is
project a, environment b:c. Splitting on every colon would silently truncate
an environment component and grant or deny access to the wrong thing — the
parser refuses to have that bug regardless of what the slug grammar currently
permits.
Working with projects
prk projects listapi API service 3 environment(s)billing Billing 2 environment(s)prk projects get apilist is every project you can see, one line each. get is one project in full
— its id, its description, and how many environments it holds.
get answers NOT_FOUND both for a project that does not exist and for one no
grant of yours covers, so it tells you whether you can reach api, never
whether the slug is taken. Creating it is what settles that, with a CONFLICT
if it is.
Full flags: prk projects.
Deleting a project takes everything with it
prk projects rm api deletes the project and everything in it. Foreign keys
are enforced by D1, so ON DELETE CASCADE actually fires: environments,
secrets, version history and any grants scoped to them go with it.
Audit rows are the exception. They carry no foreign key to the identity or the project they name, precisely so that history survives the thing it describes.
Working with environments
prk env list --project apiproduction Production rev 42 12 secret(s)staging Staging rev 7 12 secret(s)Full flags: prk env.
There is no rename and no reparent. Renaming the display name is an ordinary update; changing which project an environment belongs to is not an operation that exists.
Why reparenting is absent
environments.id and environments.project_id are contractually immutable.
project_id is deliberately excluded from the encryption AAD so that a
future reparent would not require decrypting and re-encrypting every value in
the project. The price of that choice is that the immutability has to hold. See
Encryption.
Optimistic concurrency
Every environment carries a rev counter, bumped on each mutation of its
secrets. It is the third column of prk env list.
Use it to make a full-environment replace safe against a concurrent change:
rev=$(prk env list --project api --json | jq -r '.[] | select(.slug == "production") | .rev')prk secrets upload .env --project api --env production --expected-rev "$rev"If the revision moved between those two commands, the write is refused with
PRECONDITION_FAILED (HTTP 412, exit code 6) and the environment is left
byte-for-byte unchanged. Read the current revision, re-apply your change, and
try again.
When something goes wrong
| Symptom | Meaning |
|---|---|
NOT_FOUND on a project you believe exists |
It does not exist or it is not visible to you. The API deliberately does not distinguish the two, because a 403/404 split turns the API into an oracle for which names are in use |
CONFLICT on create |
The slug is already taken at that scope |
PRECONDITION_FAILED |
--expected-rev did not match |
VALIDATION_FAILED |
The slug or name broke a rule in the table above |
Next steps
- Secrets — put something in an environment.
- Onboard a new service — the whole sequence, worked through.
- Access control