diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml index 73f257ae..c2fd9efb 100644 --- a/.github/workflows/pr-quality.yml +++ b/.github/workflows/pr-quality.yml @@ -111,6 +111,34 @@ jobs: - name: Check error-reference completeness run: pnpm check:error-reference + skill-packaging: + name: Skill Packaging + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: .node-version + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Packs the `prisma` package the same way publish does and reads the + # skill back out of the tarball: the stamp, the file set, and byte + # equality with the tracked skills/ tree. + - name: Check skill packaging + run: pnpm check:skill-packaging + test: name: Test runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index cd888bb2..81106b38 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ tmp/ # Build and test output artifacts/ dist/ +# Staged by scripts/stage-skills.mjs at prepack; source of truth is /skills +/packages/prisma/skills/ .publish/ coverage/ .vitest/ diff --git a/package.json b/package.json index c9ae9178..0aea0af4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build:compute": "pnpm --filter @prisma/compute build", "check:grammar": "turbo run check:grammar", "check:error-reference": "node scripts/list-error-codes.mjs --verify docs/reference/error-reference.md", + "check:skill-packaging": "node scripts/check-skill-packaging.mjs", "format": "biome format . --write", "lint": "biome check . --error-on-warnings", "lint:fix": "biome check . --write", diff --git a/packages/cli/src/lib/skills/allowlist.ts b/packages/cli/src/lib/skills/allowlist.ts index 9bb836a0..f5089729 100644 --- a/packages/cli/src/lib/skills/allowlist.ts +++ b/packages/cli/src/lib/skills/allowlist.ts @@ -18,6 +18,9 @@ export const SKILL_SOURCE_PACKAGES: readonly string[] = [ "@prisma/orm-sqlite", "@prisma/orm-mongo", "@prisma/composer", + // The CLI's own package: ships prisma-platform-core-concepts, staged + // into the tarball by scripts/stage-skills.mjs at prepack. + "prisma", ]; /** The directory inside a source package's tarball that holds its skill diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 2cdbd9e7..b13ec089 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -15,6 +15,7 @@ }, "files": [ "dist", + "skills", "README.md", "LICENSE" ], @@ -43,7 +44,7 @@ "license": "Apache-2.0", "scripts": { "build": "tsdown", - "prepack": "pnpm run build", + "prepack": "pnpm run build && node ../../scripts/stage-skills.mjs", "typecheck": "tsc --noEmit", "test": "vitest run" }, diff --git a/scripts/check-skill-packaging.mjs b/scripts/check-skill-packaging.mjs new file mode 100644 index 00000000..6b82fbea --- /dev/null +++ b/scripts/check-skill-packaging.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +// The `prisma-platform-core-concepts` skill must arrive in the `prisma` +// tarball, carrying the version of the tarball it arrived in. +// +// That claim is only worth as much as the artifact that proves it. The +// manifest can list `"skills"` in `files` while `prepack` failed to stage +// anything; the staged copy can be a leftover from an older version; the +// frontmatter stamp can be missing because someone hand-edited the skill. +// None of those show up in a unit test — they show up in what npm uploads. +// So this check packs the package the same way the publish workflow does and +// reads the skill back out of the tarball. +// +// It also compares the packed skill byte-for-byte against the repo-root +// `skills/` tree, which is the tracked source. If those two ever disagree, +// the tarball is serving different instructions than the repo records. +// +// Usage: node scripts/check-skill-packaging.mjs + +import { execFileSync } from "node:child_process"; +import { + existsSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { readSkillFrontmatter } from "./skill-frontmatter.ts"; + +const PACKAGE = "prisma"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const packageDir = join(repoRoot, "packages/prisma"); +const skillsRoot = join(repoRoot, "skills"); + +const failures = []; +function require_(condition, message) { + if (!condition) failures.push(message); +} + +/** Every file under `dir`, as paths relative to it, sorted. */ +function filesUnder(dir) { + return readdirSync(dir, { recursive: true, encoding: "utf-8" }) + .filter((entry) => statSync(join(dir, entry)).isFile()) + .sort(); +} + +/** The skills the repo-root tree says belong to this package. */ +function ownedSkillNames() { + return readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .filter((entry) => existsSync(join(skillsRoot, entry.name, "SKILL.md"))) + .filter( + (entry) => + readSkillFrontmatter( + readFileSync(join(skillsRoot, entry.name, "SKILL.md"), "utf-8"), + ).library === PACKAGE, + ) + .map((entry) => entry.name); +} + +const expected = ownedSkillNames(); +require_( + expected.length > 0, + `no skill under skills/ declares \`metadata.library: "${PACKAGE}"\` — the frontmatter is what routes a skill into a tarball, so without it nothing ships.`, +); + +// pnpm pack, not npm pack: it runs the same `prepack` and rewrites the same +// specifiers a real publish does. +const work = mkdtempSync(join(tmpdir(), "skill-packaging-")); +try { + execFileSync("pnpm", ["pack", "--pack-destination", work], { + cwd: packageDir, + stdio: ["ignore", "ignore", "inherit"], + }); + const tarball = readdirSync(work).find((f) => f.endsWith(".tgz")); + if (tarball === undefined) + throw new Error(`pnpm pack produced no tarball for ${PACKAGE}`); + execFileSync("tar", ["xzf", tarball], { cwd: work }); + const packedRoot = join(work, "package"); + + const packedVersion = JSON.parse( + readFileSync(join(packedRoot, "package.json"), "utf-8"), + ).version; + + for (const name of expected) { + const packedSkillDir = join(packedRoot, "skills", name); + const packedSkill = join(packedSkillDir, "SKILL.md"); + if (!existsSync(packedSkill)) { + require_( + false, + `the packed tarball has no skills/${name}/SKILL.md — check that "skills" is in the package's \`files\` and that the \`prepack\` script runs scripts/stage-skills.mjs.`, + ); + continue; + } + + const { library, libraryVersion } = readSkillFrontmatter( + readFileSync(packedSkill, "utf-8"), + ); + require_( + library === PACKAGE, + `the packed skills/${name}/SKILL.md declares metadata.library: "${library}", not "${PACKAGE}".`, + ); + require_( + libraryVersion === packedVersion, + `the packed skills/${name}/SKILL.md is stamped metadata.library_version: "${libraryVersion}" but ships in ${PACKAGE}@${packedVersion} — the stamp is what tells a reader which CLI surface the skill describes. Run \`node scripts/set-version.ts ${packedVersion}\`.`, + ); + + // Byte equality with the tracked source: the tarball must serve exactly + // the instructions the repo records. + const sourceDir = join(skillsRoot, name); + const sourceFiles = filesUnder(sourceDir); + const packedFiles = filesUnder(packedSkillDir); + require_( + sourceFiles.join("\n") === packedFiles.join("\n"), + `the packed skills/${name}/ does not have the same files as ${relative(repoRoot, sourceDir)}/ (packed: ${packedFiles.join(", ")}; source: ${sourceFiles.join(", ")}).`, + ); + for (const file of sourceFiles.filter((f) => packedFiles.includes(f))) { + require_( + readFileSync(join(sourceDir, file), "utf-8") === + readFileSync(join(packedSkillDir, file), "utf-8"), + `the packed skills/${name}/${file} differs from the tracked ${relative(repoRoot, join(sourceDir, file))}.`, + ); + } + } +} finally { + rmSync(work, { recursive: true, force: true }); +} + +if (failures.length > 0) { + process.stderr.write( + `\nFAIL — skill packaging check:\n${failures.map((f) => ` - ${f}\n`).join("")}`, + ); + process.exit(1); +} +process.stderr.write( + `\nOK — the ${PACKAGE} tarball carries ${expected.join(", ")}, stamped with its version and identical to skills/.\n`, +); diff --git a/scripts/set-version.ts b/scripts/set-version.ts index 72cd0932..3ac0ab0c 100644 --- a/scripts/set-version.ts +++ b/scripts/set-version.ts @@ -9,6 +9,7 @@ import { participatesInLockstep, rewriteWorkspaceDeps, } from "./set-version-utils.ts"; +import { stampSkillVersion } from "./skill-frontmatter.ts"; // Operator rulings: `@prisma/compute` versions independently pending // extraction to another repo (2026-08-10), and `@prisma/cli-engine` @@ -114,4 +115,28 @@ for (const manifestPath of trackedManifests) { updatedCount++; } -console.log(`\nDone! Updated ${updatedCount} packages.`); +// Skills ship inside the `prisma` tarball and their frontmatter names +// the version of the CLI they describe (`metadata.library_version`). +// The `prisma` package versions in lockstep, so every skill is stamped +// with the same root version; check-skill-packaging.mjs verifies the +// stamp against the packed tarball. +const skillsDir = path.join(rootDir, "skills"); +const skillDirEntries = await fs + .readdir(skillsDir, { withFileTypes: true }) + .catch(() => []); +let stampedSkills = 0; +for (const entry of skillDirEntries) { + if (!entry.isDirectory()) continue; + const skillPath = path.join(skillsDir, entry.name, "SKILL.md"); + // biome-ignore lint/performance/noAwaitInLoops: each skill prints its "Stamped" line as it is rewritten, so a run that fails part way through leaves an accurate record of which files already changed. + const source = await fs.readFile(skillPath, "utf-8").catch(() => undefined); + if (source === undefined) continue; + + await fs.writeFile(skillPath, stampSkillVersion(source, version)); + console.log(`Stamped ${path.relative(rootDir, skillPath)} with ${version}`); + stampedSkills++; +} + +console.log( + `\nDone! Updated ${updatedCount} packages and ${stampedSkills} skills.`, +); diff --git a/scripts/skill-frontmatter.ts b/scripts/skill-frontmatter.ts new file mode 100644 index 00000000..e960a68e --- /dev/null +++ b/scripts/skill-frontmatter.ts @@ -0,0 +1,109 @@ +// Pure helpers for the `metadata.library` / `metadata.library_version` +// frontmatter keys that tie a skill to the npm package it describes. +// +// The skill ships inside the `prisma` tarball, so the version a reader +// sees must be the version they installed. `set-version.ts` stamps +// `library_version` from the same root version it writes into every +// package.json, and `check-skill-packaging.mjs` re-reads it out of the +// packed tarball. Both go through here so there is one definition of +// what the frontmatter looks like. +// +// Under `metadata:` rather than at the top level because the Agent +// Skills spec (agentskills.io) defines the top-level key set and +// reserves `metadata` — a string→string map — for exactly this kind of +// publisher extension. A top-level `library:` would be an undefined key +// that a strict runtime is entitled to reject. + +const FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/; + +/** The `metadata:` mapping: the key line plus the indented block under it. */ +const METADATA_SECTION = /^metadata:[ \t]*\r?\n((?:[ \t]+\S.*(?:\r?\n|$))*)/m; + +const LEADING_INDENT = /^[ \t]+/; + +function keyPattern(key: string): RegExp { + return new RegExp(`^[ \\t]+${key}:[ \\t]*(.*)$`, "m"); +} + +function unquote(value: string): string { + const trimmed = value.trim(); + const quote = trimmed[0]; + if ( + (quote === '"' || quote === "'") && + trimmed.endsWith(quote) && + trimmed.length >= 2 + ) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +export interface SkillFrontmatter { + /** The npm package the skill ships inside, e.g. `prisma`. */ + library?: string; + /** The version of that package, stamped at release time. */ + libraryVersion?: string; +} + +/** The indented body of the frontmatter's `metadata:` map, if it has one. */ +function metadataBlock(source: string): string | undefined { + const frontmatter = FRONTMATTER_BLOCK.exec(source)?.[1]; + if (frontmatter === undefined) return undefined; + return METADATA_SECTION.exec(frontmatter)?.[1]; +} + +/** Read the version-stamp keys out of a `SKILL.md`. Absent keys stay undefined. */ +export function readSkillFrontmatter(source: string): SkillFrontmatter { + const metadata = metadataBlock(source); + if (metadata === undefined) return {}; + + const library = keyPattern("library").exec(metadata)?.[1]; + const libraryVersion = keyPattern("library_version").exec(metadata)?.[1]; + return { + library: library === undefined ? undefined : unquote(library), + libraryVersion: + libraryVersion === undefined ? undefined : unquote(libraryVersion), + }; +} + +/** + * Rewrite `metadata.library_version` in a `SKILL.md` to `version`. Idempotent. + * + * Both keys must already be present. Adding them is an authoring decision — + * a skill that ships in a tarball declares which package it ships in — and + * silently inserting them here would let a skill with no `library` key be + * stamped with a version that means nothing. + */ +export function stampSkillVersion(source: string, version: string): string { + if (FRONTMATTER_BLOCK.exec(source)?.[1] === undefined) { + throw new Error("SKILL.md has no YAML frontmatter block."); + } + const metadata = metadataBlock(source); + if (metadata === undefined) { + throw new Error( + "SKILL.md frontmatter has no `metadata` map — the version stamp lives there, because the Agent Skills spec reserves `metadata` for publisher keys and defines the top-level ones.", + ); + } + const { library, libraryVersion } = readSkillFrontmatter(source); + if (library === undefined) { + throw new Error( + "SKILL.md frontmatter has no `metadata.library` key — a skill that ships inside a tarball must name the package it ships in.", + ); + } + if (libraryVersion === undefined) { + throw new Error( + "SKILL.md frontmatter has no `metadata.library_version` key — add it (any string) so the release pipeline can stamp it.", + ); + } + + // Function replacements throughout: the skill body and its long + // `description` are prose, and a literal `$&` or `$1` in them would be + // expanded as a capture reference by the string form. The version is + // quoted because `metadata` is a string→string map. + const stamped = metadata.replace( + keyPattern("library_version"), + (line) => + `${LEADING_INDENT.exec(line)?.[0] ?? " "}library_version: "${version}"`, + ); + return source.replace(metadata, () => stamped); +} diff --git a/scripts/stage-skills.mjs b/scripts/stage-skills.mjs new file mode 100644 index 00000000..6137f57f --- /dev/null +++ b/scripts/stage-skills.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Stage the agent skills that belong to a package into that package's +// `skills/` directory, so they travel inside its tarball. +// +// Why in the tarball: an agent skill is only useful if it describes the +// version of the package the reader actually installed. Shipping it beside +// the code makes that true by construction — the skill and the CLI surface it +// documents are the same artifact, updated by the same `pnpm add`. This is +// also what lets a consumer's `prisma skills sync` resolve the skill by +// package name instead of scanning node_modules. +// +// Which skills belong to which package is not configured here: each +// `SKILL.md` already declares its package in `metadata.library` frontmatter +// (the same key `set-version.ts` stamps the version onto), so this script +// reads that and copies the matching trees. One source of truth, no table to +// keep in sync. +// +// Run from the package directory, which is where npm/pnpm run `prepack`: +// +// node ../../scripts/stage-skills.mjs +// +// `prepack` rather than the build: `pnpm pack` and `pnpm publish` both run +// it, so the staged copy is produced by the very act of making a tarball and +// can never be a stale build artifact restored from a turbo cache. + +import { + cpSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { readSkillFrontmatter } from "./skill-frontmatter.ts"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const packageDir = process.cwd(); + +const packageName = JSON.parse( + readFileSync(join(packageDir, "package.json"), "utf-8"), +).name; +if (typeof packageName !== "string") { + throw new Error(`No package name in ${join(packageDir, "package.json")}.`); +} + +const skillsRoot = join(repoRoot, "skills"); +const owned = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .filter((entry) => existsSync(join(skillsRoot, entry.name, "SKILL.md"))) + .filter((entry) => { + const source = readFileSync( + join(skillsRoot, entry.name, "SKILL.md"), + "utf-8", + ); + return readSkillFrontmatter(source).library === packageName; + }) + .map((entry) => entry.name); + +const destinationRoot = join(packageDir, "skills"); + +// Rebuilt from scratch every run: a skill that stops belonging to this +// package, or is renamed, must not linger in the next tarball. +rmSync(destinationRoot, { recursive: true, force: true }); + +if (owned.length === 0) { + process.stderr.write( + `No skills declare metadata.library: "${packageName}" — nothing staged.\n`, + ); + process.exit(0); +} + +mkdirSync(destinationRoot, { recursive: true }); +for (const name of owned) { + cpSync(join(skillsRoot, name), join(destinationRoot, name), { + recursive: true, + }); +} + +process.stderr.write( + `Staged ${owned.join(", ")} into ${packageName}'s tarball.\n`, +); diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..aeeb0d3d --- /dev/null +++ b/skills/README.md @@ -0,0 +1,66 @@ +# Prisma Platform skills + +Agent skills for the [Prisma CLI](https://github.com/prisma/prisma-cli): one +`SKILL.md` that teaches an LLM agent the Prisma Platform's resource model +without re-deriving it from documentation each time. + +## What's in the box + +One skill, `prisma-platform-core-concepts`, covering the platform: the +workspace/project/branch model, preview environments, the two deploy paths +(GitHub integration and `prisma deploy`), services and versions, the Compute +runtime, Prisma Postgres, object storage, environment variables, the local +development stack, and the failure modes. The ORM and Composer have their own +core-concepts skills that ship inside their own packages; this one owns +everything platform-side. + +## Install + +The skill ships inside the `prisma` tarball, so installing the package is +what brings it in. `prisma skills sync` copies it out of `node_modules` into +the skill directories the agent runtimes read (`.claude/skills/`, +`.cursor/skills/`, `.agents/skills/`, `.devin/skills/`): + +```bash +pnpm add -D prisma +pnpm prisma skills sync +``` + +`prisma init` wires a `postinstall` hook so an upgrade brings the matching +skill with it. The version you read is then always the version you +installed: the skill's frontmatter carries `metadata.library: "prisma"` and a +`metadata.library_version` stamped by the release that built the tarball +([`scripts/set-version.ts`](../scripts/set-version.ts); +[`scripts/check-skill-packaging.mjs`](../scripts/check-skill-packaging.mjs) +proves it against the packed artifact). + +## Authoring rules + +For anyone editing the skill: + +1. **Verify every claim while drafting, not in a final pass.** Every command + and flag must exist in `packages/cli/src/` (the `mountedCommands` map in + `cli.ts` is the source of truth for what mounts where). If a search finds + nothing, the surface doesn't ship: name it under *What the platform + doesn't do yet* instead of extrapolating. +2. **The skill must be self-contained.** It gets installed into other repos, + so no link may resolve outside `skills/prisma-platform-core-concepts/`. + Repo docs may be named in prose, never linked relatively. +3. **Teach concepts, not procedures.** Name the moving parts and the command + that reveals each piece of state; reserve numbered steps for + one-safe-path operations. +4. **Leave the `metadata` stamp alone.** `metadata.library` names the npm + package the skill ships inside, and `metadata.library_version` is + rewritten by [`scripts/set-version.ts`](../scripts/set-version.ts) on + every release. Hand-editing the version, or dropping either key, breaks + the release script and + [`scripts/check-skill-packaging.mjs`](../scripts/check-skill-packaging.mjs). + They live under `metadata` because the Agent Skills spec + (agentskills.io) defines the top-level keys and reserves that map + (string → string) for publisher extensions. A new skill needs both keys, + with any placeholder version. +5. **Folder name and frontmatter `name` must match.** The runtimes key on + the frontmatter, humans on the folder. + +Maintainer-facing skills (release process and similar) live in +[`../skills-contrib/`](../skills-contrib/), not here. diff --git a/skills/prisma-platform-core-concepts/SKILL.md b/skills/prisma-platform-core-concepts/SKILL.md new file mode 100644 index 00000000..1d210ca0 --- /dev/null +++ b/skills/prisma-platform-core-concepts/SKILL.md @@ -0,0 +1,304 @@ +--- +name: prisma-platform-core-concepts +metadata: + library: "prisma" + library_version: "8.0.0-rc.11" + version: 2026.8.28 +description: >- + Use when hosting, deploying, or operating an app on the Prisma Platform: + projects, branches, preview environments, services and their versions, + Prisma Postgres databases, object-store buckets, environment variables, + custom domains, logs, or the GitHub integration. Triggers on "Prisma + Platform", "Prisma Compute", "Prisma Postgres", "Prisma Storage", + "preview environment", `prisma deploy`, `prisma dev`, `prisma auth`, + `prisma project`, `prisma branch`, `prisma service`, `prisma postgres`, + `prisma bucket`, `prisma git`, promote, rollback. +--- + +# Prisma Platform core concepts + +> **Isolated infrastructure for every branch.** + +The Prisma Platform moves fast, and your training data about it is very +likely outdated. This skill ships inside the installed `prisma` package, so +it describes the exact version this project has: treat it as the source of +truth over anything you remember about Prisma hosting. If +`metadata.library_version` in this file's frontmatter does not match the +project's installed `prisma` package, run `prisma skills sync` and re-read. +Fuller documentation for everything here lives at prisma.io/docs; consult it +when a concept needs more depth than this file carries. + +This file shares the platform's structures, hierarchies, relationships, and +workflows. It is not a CLI reference: commands appear only where a workflow +needs them. To learn a command surface, run `prisma --help`, or +`prisma --help` for any group named below; most commands accept +`--json` for machine-readable output. The data contract, migrations, and +queries belong to the `prisma-orm-core-concepts` skill; declaring services +and modules in code belongs to `prisma-composer-core-concepts`. + +## The stack + +One CLI, `prisma`, fronts a set of products designed to be used together: + +| Product | What it is | Concepts live in | +| --- | --- | --- | +| Prisma ORM (Prisma 8) | Data contract, typed queries, migrations | `prisma-orm-core-concepts` | +| Prisma Composer | The app declaration: services, databases, buckets, wiring | `prisma-composer-core-concepts` | +| Prisma Compute | Hosting: runs your services next to your data | this skill | +| Prisma Postgres | Managed PostgreSQL | this skill | +| Prisma Storage | S3-compatible object storage | this skill | + +**Every deployed app is a Composer app.** Your server code plus a declaration +(`module.ts`) naming its services, databases, and buckets. The declaration is +the source of truth: deploying converges the platform to it, re-deploying +applies only the difference, and removing a resource from the module removes +it from the platform on the next deploy. There is no separate provisioning +step and no connection strings to wire by hand; Composer injects them. + +## The resource model + +1. **Workspace**: the account boundary. Members, billing, and projects live + here; the `auth` group manages which one you act in. +2. **Project**: one product or codebase. Its region is chosen at creation and + is immutable afterwards; every resource in the project inherits it. +3. **Branch**: the isolation boundary, named after a git branch. A branch is + an environment: it owns its own services, its own databases, its own + buckets, and its own environment-variable overrides. +4. Inside a branch: **services** (HTTP apps, deployed as versions), + **databases**, and **buckets**. + +The first branch of a project is the **production** branch. Every branch +after it is a **preview** branch. Production and previews differ in exactly +two ways: which environment-variable class they resolve (see below), and +lifecycle (previews can be reclaimed and torn down; production cannot). + +A CLI invocation resolves its project from a directory binding, created when +a project is created from or linked to the directory and stored in the +gitignored `.prisma/local.json`. Resource commands take flags to target +another project or branch explicitly. + +## Branches are preview environments + +There is no separate "preview environment" object to create or configure. The +branch is the environment, and branches come into being by deploying to them: + +1. **Push a git branch** (with the GitHub integration connected): the + platform creates the branch environment on the fly, builds, and deploys. +2. **Deploy a stage from the CLI**: `prisma deploy module.ts --stage pr-42` + creates a branch named `pr-42` and deploys the identical app graph into + it. A stage name must be a valid git ref name; an invalid name is a hard + error. + +A preview branch that needs a database gets a fresh, empty one, wired in as a +branch-scoped `DATABASE_URL`. Preview work can never touch production data +unless you explicitly point it there. Schema comes from your committed +migrations; data does not follow from production. + +Preview lifecycle rules: + +1. Deleting the upstream git branch tears the whole environment down: + services, versions, and databases, including their data. +2. An idle, unpinned preview branch can be reclaimed automatically. Pinning a + preview exempts it from idle reclamation, but not from teardown when its + git branch is deleted. +3. The production branch is never reclaimed or torn down this way. + +## Two ways to deploy + +**GitHub (recommended).** Install the Prisma GitHub app and connect the +repository: `prisma git connect`, or import the repository in the Console. +From then on, every push builds and deploys on the platform. A push to the +default branch deploys production; a push to any other branch creates or +updates that branch's preview environment. No workflow file is required, and +previews come free with every branch. Opening a pull request does not itself +deploy anything; previews track branch pushes. + +**CLI.** `prisma deploy module.ts` deploys production directly; add +`--stage ` for a preview. Authenticate once with `prisma auth login` +(interactive), or set `PRISMA_SERVICE_TOKEN` (plus `PRISMA_WORKSPACE_ID` +when the token can see more than one workspace) for CI and other headless +runs. The first deploy creates the project, named after your module; the +name must be unique in the workspace, and `--name` deploys under a different +one. + +Both paths converge the same declaration, so they are mutually idempotent: +a repo can be connected to GitHub and still be deployed from the CLI. + +## Services and versions + +A **service** is an HTTP app inside a branch, reachable at a stable URL. +Every deploy of a service creates a new immutable **version**; exactly one +version is live behind the stable URL at a time. Versions can be inspected, +started, stopped, and deleted individually, and a version's logs can be read +or followed; the `service` group covers all of it. + +Two movements between versions matter: + +1. **Promote** takes a version to production by rebuilding it with + production-class environment variables. A version built on a preview + branch never carries preview configuration into production. +2. **Rollback** points production back at a previous version. A stopped + target is started and health-checked before traffic switches, so rollback + is zero-downtime. + +**Custom domains** attach to a service's production branch only. The +workflow: add the domain, create the CNAME record the platform reports, and +wait for DNS verification and certificate provisioning; a verification that +failed on missing DNS is retried after the record exists. + +## The Compute runtime + +Services run on Bun, next to their Prisma Postgres database. There is no +container image to author and no platform emulator to install locally. + +An idle service **sleeps**: the platform snapshots its memory after a short +period of inactivity and resumes it from the snapshot on the next request. +Request handling is unaffected, but background work outside the request +lifecycle (`setTimeout`, `setInterval`, floating promises) can be +interrupted mid-flight. The `@prisma/compute` package provides the two +keep-awake primitives: + +1. `waitUntil(promise, { signal })` keeps the instance awake until the + promise settles. +2. `using guard = new KeepAwakeGuard({ signal })` holds the instance awake + for a scope; call `.release()` in a `finally` block where `using` is + unavailable. + +Pass `AbortSignal.timeout(ms)` as a cost bound: the signal releases the +guard, it does not cancel your work. Do not build schedulers on `setInterval` +inside a service; sleeping makes them silently unreliable. + +## Prisma Postgres + +To your application, a Prisma Postgres database is a regular PostgreSQL +database behind a connection string: any client works, including Prisma ORM, +psql, Kysely, and Drizzle. On top of plain PostgreSQL each database has +built-in connection pooling (no PgBouncer to run), optional query caching, +and automated backups. + +Databases usually come from deploying a module that declares one. They can +also be created directly, in which case the connection URL prints exactly +once. Additional connection strings can be minted and rotated per database; +every secret is shown exactly once at creation, and no later read reveals +it. Backups are automated, listable, and restorable, and per-database usage +metrics are queryable; the `postgres` group covers all of it. + +## Object storage + +Buckets are S3-compatible object stores that live inside the project, +optionally associated with a branch (the project's default branch when +omitted). A bucket cannot be renamed. + +Access keys are bucket-scoped credentials with a role of `read` or +`read_write`. Creating one prints the access key id, secret, endpoint, and +provider bucket name exactly once; a lost secret is revoked and re-created, +never recovered. The `bucket` group manages both buckets and their keys. + +On Compute, Bun's built-in S3 client picks up `S3_ENDPOINT`, `S3_BUCKET`, +`S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY` from the environment, so a +bucket declared in the module needs no client configuration. Locally, a +Composer-declared bucket gets a local stand-in with the same S3 surface; +code written against it runs unchanged on the platform. + +## Environment variables + +Configuration has two independent axes: + +| Axis | Values | Meaning | +| --- | --- | --- | +| Class | `production`, `preview` | Which branches resolve it: production-class values reach the production branch, preview-class values reach every preview | +| Scope | project template, branch override | A template applies to all branches of its class; an override pins a value to one named preview branch | + +Rules that bite: + +1. Branch overrides exist only for the preview class. Production + configuration is always the project template; there is no way to give one + production deploy a special value. +2. Values are write-only. Listing shows names and scopes, never values, and + no API returns them. To change a value you replace it. +3. Variables resolve at deploy time from the branch's class. You never pass + environment variables as part of a deploy. +4. Templates, per-branch overrides, and wholesale dotenv import are all + managed through the `project env` group. +5. `DATABASE_URL` is managed for you when the module declares a database: + previews get their own fresh database's URL, production gets the + production database's. Only set it yourself for databases the platform + does not manage. + +## Local development + +The local stack is real, fast, and needs no cloud credentials. Prefer it +over deploying whenever you want to show the user a change or verify your +own work: + +1. Build the app exactly as you would for deploy, then run + `prisma dev module.ts`. It provisions a local Prisma Postgres database, + applies the schema, creates local stand-ins for declared buckets, starts + the services, and prints a local URL. It watches built output and + restarts a service when its build changes. +2. Exercise the running app directly (for example with `curl` against the + printed URL) instead of reasoning about what the code would do. +3. Stopping `dev` leaves local databases, buckets, and their data in place; + the next run is a warm start. `--fresh` wipes this app's local instances + and data first. +4. Deploying is the same pipeline pointed at the platform: switching from + local to hosted is a configuration change, not a code change. Reach for + `prisma deploy --stage ` only when you need a shareable URL or + platform behaviour (sleeping, real DNS, production-like config). +5. For pure frontend iteration the framework's own dev server also works; + the Composer local stack is what mirrors the deployed wiring. + +Windows is not supported for the local stack yet. + +## Failure modes quick reference + +1. **A secret you need was shown once and is gone.** By design: database + URLs, connection strings, and bucket keys print exactly once at creation. + Rotate or re-create; do not hunt for a read path, none exists. +2. **Deploy refuses the module name.** Module names are workspace-unique. + Deploy under `--name `, or delete the conflicting project. +3. **A preview environment vanished.** Its git branch was deleted, or it + sat idle unpinned and was reclaimed. Push or deploy the branch again to + re-create it; the database starts empty again. +4. **Background work never ran in production.** The service slept. + Wrap the work in `waitUntil` or a `KeepAwakeGuard`, or move it out of the + service. +5. **A custom domain stays unverified.** The CNAME to the reported target + must exist first. Fix DNS, then retry the verification. +6. **Production shows a stale value after an environment-variable change.** + Variables are resolved at deploy time. Re-deploy (or promote) so the new + value is picked up. +7. **A stage name is rejected.** Stage names must be valid git ref names; + rename it. +8. **A resource disappeared from the platform after a deploy.** It was + removed from `module.ts`; the module is the source of truth and deploy + converges to it. Restore the declaration and re-deploy. + +## What the platform doesn't do yet + +Name the gap instead of inventing an API: + +1. **No branch create or delete from the CLI.** The `branch` group only + lists. Branches are created by pushing or by deploying a stage, and + removed by deleting the upstream git branch; there is no CLI teardown + verb for a stage yet. +2. **No build retry.** A failed build is finished; the retry is the next + push or the next deploy. +3. **No reading environment-variable values back**, anywhere. This is a + security stance, not a missing feature, but agents look for it often + enough to name here. +4. **No bucket rename** and no bucket-to-branch re-association after + creation; re-create instead. + +For anything else missing, run the nearest group with `--help` before +concluding it does not exist, and route requests with `prisma feedback` +rather than guessing. + +## Related skills + +Everything inside the app is owned by the siblings: the data contract, +queries, and migrations by `prisma-orm-core-concepts` (and +`prisma-orm-migrations`), and the declaration of services, modules, wiring, +and testing seams by `prisma-composer-core-concepts`. The skills install +together via `prisma skills sync`; when a task crosses from platform +resources into app code, switch skills instead of guessing.