From b103cc93cc520c93588e5ffc38db4e4f7b92d518 Mon Sep 17 00:00:00 2001
From: Michael Ramos
Date: Wed, 2 Sep 2026 12:13:34 -0700
Subject: [PATCH 1/5] fix(site): restore manual agent install guidance
---
apps/site/src/pages/index.astro | 49 ++++++++-----
apps/site/src/styles/landing.css | 120 ++++++++++++++++++++++++-------
2 files changed, 128 insertions(+), 41 deletions(-)
diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro
index 691b1b7..33012ba 100644
--- a/apps/site/src/pages/index.astro
+++ b/apps/site/src/pages/index.astro
@@ -78,18 +78,15 @@ When the setup is complete, begin your final response with “Artifact Server is
during product development. Publish, review, comment on, version, and share
the same work. The built-in MCP server gives agents direct access.
@@ -340,10 +357,10 @@ When the setup is complete, begin your final response with “Artifact Server is
try {
await navigator.clipboard.writeText(text);
button.dataset.copied = "true";
- button.setAttribute("aria-label", "Artifact Server setup prompt copied");
+ button.setAttribute("aria-label", button.dataset.copySuccessLabel ?? "Copied");
window.setTimeout(() => {
delete button.dataset.copied;
- button.setAttribute("aria-label", "Copy the Artifact Server setup prompt");
+ button.setAttribute("aria-label", button.dataset.copyLabel ?? "Copy");
}, 1600);
} catch {
// Clipboard unavailable (insecure context or permission denied).
diff --git a/apps/site/src/styles/landing.css b/apps/site/src/styles/landing.css
index 5537f66..1459885 100644
--- a/apps/site/src/styles/landing.css
+++ b/apps/site/src/styles/landing.css
@@ -233,14 +233,13 @@
}
.landing-agent-setup {
- display: flex;
+ display: grid;
max-width: 36rem;
margin-top: 1.25rem;
- gap: 0.75rem;
+ gap: 0.5rem;
}
-.landing-copy-prompt,
-.landing-agent-setup__docs {
+.landing-copy-prompt {
display: flex;
min-height: 3.25rem;
align-items: center;
@@ -258,7 +257,6 @@
.landing-copy-prompt {
min-width: 0;
- flex: 1 1 auto;
gap: 0.875rem;
padding: 0.75rem 0.875rem;
font: inherit;
@@ -286,7 +284,8 @@
}
.landing-copy-prompt__label {
- min-width: 5.75rem;
+ min-width: 0;
+ flex: 1;
font-size: 0.875rem;
font-weight: 650;
text-align: left;
@@ -310,8 +309,7 @@
color: var(--nb-success);
}
-.landing-copy-prompt__icon,
-.landing-agent-setup__docs svg {
+.landing-copy-prompt__icon {
display: grid;
width: 1rem;
height: 1rem;
@@ -319,26 +317,87 @@
place-items: center;
}
-.landing-agent-setup__docs {
- flex: none;
- gap: 0.5rem;
- padding: 0.75rem 1rem;
- font-size: 0.8125rem;
- font-weight: 650;
+.landing-manual-command {
+ display: grid;
+ min-height: 2.75rem;
+ grid-template-columns: minmax(0, 1fr) auto 2rem;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.375rem 0.375rem 0.375rem 0.875rem;
+ border: 1px solid var(--nb-border);
+ background: var(--nb-card);
+ font-family: var(--nb-font-mono);
+ font-size: 0.6875rem;
+}
+
+.landing-manual-command code {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--nb-foreground);
+ text-overflow: ellipsis;
white-space: nowrap;
}
+.landing-manual-command a {
+ color: var(--nb-muted-foreground);
+ text-decoration: none;
+ white-space: nowrap;
+ transition: color 150ms ease;
+}
+
+.landing-copy-icon-button {
+ display: grid;
+ width: 2rem;
+ height: 2rem;
+ place-items: center;
+ border: 1px solid var(--nb-border);
+ background: transparent;
+ color: var(--nb-muted-foreground);
+ cursor: pointer;
+ transition:
+ border-color 150ms ease,
+ color 150ms ease,
+ transform 150ms ease;
+}
+
+.landing-copy-icon-button svg {
+ width: 0.875rem;
+ height: 0.875rem;
+}
+
+.landing-copy-icon-button svg:last-child,
+.landing-copy-icon-button[data-copied] svg:first-child {
+ display: none;
+}
+
+.landing-copy-icon-button[data-copied] {
+ border-color: color-mix(in oklch, var(--nb-success) 70%, var(--nb-border));
+ color: var(--nb-success);
+}
+
+.landing-copy-icon-button[data-copied] svg:last-child {
+ display: block;
+}
+
.landing-copy-prompt:active,
-.landing-agent-setup__docs:active {
+.landing-copy-icon-button:active {
transform: scale(0.96);
}
@media (hover: hover) {
- .landing-copy-prompt:hover,
- .landing-agent-setup__docs:hover {
+ .landing-copy-prompt:hover {
border-color: var(--nb-foreground);
background: var(--nb-muted);
}
+
+ .landing-manual-command a:hover,
+ .landing-copy-icon-button:hover {
+ color: var(--nb-foreground);
+ }
+
+ .landing-copy-icon-button:hover {
+ border-color: var(--nb-foreground);
+ }
}
/* Hero: the full-screen review — artifact canvas plus the open comment panel. */
@@ -1256,18 +1315,20 @@
}
.landing-agent-setup {
- align-items: stretch;
- flex-direction: column;
+ width: 100%;
}
.landing-copy-prompt {
- justify-content: flex-start;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ justify-items: start;
gap: 0.625rem;
padding-inline: 0.625rem;
}
.landing-agent-setup__marks {
- flex: 1 1 auto;
+ grid-column: 1;
+ grid-row: 1;
gap: 0.25rem;
}
@@ -1278,15 +1339,28 @@
}
.landing-copy-prompt__label {
+ grid-column: 1 / -1;
+ grid-row: 2;
min-width: auto;
font-size: 0.8125rem;
}
.landing-copy-prompt__icon {
+ grid-column: 2;
+ grid-row: 1;
width: 0.875rem;
height: 0.875rem;
}
+ .landing-manual-command {
+ grid-template-columns: minmax(0, 1fr) 2rem;
+ }
+
+ .landing-manual-command a {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ }
+
.artifact-window {
height: 30rem;
box-shadow: 0.75rem 0.75rem 0 color-mix(in oklch, var(--nb-primary) 12%, transparent);
@@ -1300,10 +1374,6 @@
grid-template-columns: 1fr;
}
- .landing-agent-setup__docs {
- align-self: flex-start;
- }
-
.artifact-canvas {
display: none;
}
From b8ba6c602fa8099e40a56c0de62fdde2985e6383 Mon Sep 17 00:00:00 2001
From: Michael Ramos
Date: Wed, 2 Sep 2026 12:16:21 -0700
Subject: [PATCH 2/5] fix(site): link install prompt to markdown docs
---
apps/site/scripts/verify-agent-surfaces.mjs | 21 +++++++++++++++++++++
apps/site/src/pages/index.astro | 10 +++++-----
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/apps/site/scripts/verify-agent-surfaces.mjs b/apps/site/scripts/verify-agent-surfaces.mjs
index f29c0ca..26c9fa6 100644
--- a/apps/site/scripts/verify-agent-surfaces.mjs
+++ b/apps/site/scripts/verify-agent-surfaces.mjs
@@ -21,6 +21,27 @@ assert.ok(llmsFiles.some((file) => file === join(outputDirectory, "llms.txt")),
const rootLlms = readFileSync(join(outputDirectory, "llms.txt"), "utf8");
assert.match(rootLlms, /^# Artifact Server\n\n> /, "/llms.txt must start with an H1 and project summary blockquote.");
+const homepage = readFileSync(join(outputDirectory, "index.html"), "utf8");
+const installPromptButton = [...homepage.matchAll(//g)]
+ .map((match) => match[0])
+ .find((button) => attribute(button, "data-copy-label") === "Copy the Artifact Server agent install prompt");
+assert.ok(installPromptButton, "The homepage is missing the agent install prompt.");
+
+const installPrompt = attribute(installPromptButton, "data-copy");
+assert.ok(installPrompt, "The agent install prompt is empty.");
+for (const path of [
+ "/docs/get-started/index.md",
+ "/docs/deploy/index.md",
+ "/docs/mcp/index.md",
+ "/docs/agents/index.md",
+ "/docs/index.md",
+]) {
+ assert.ok(
+ installPrompt.includes(new URL(path, canonicalOrigin).href),
+ `The agent install prompt does not link to ${path}.`,
+ );
+}
+
for (const htmlFile of htmlFiles) {
const html = readFileSync(htmlFile, "utf8");
const htmlPath = publicPath(htmlFile);
diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro
index 33012ba..3ed93fd 100644
--- a/apps/site/src/pages/index.astro
+++ b/apps/site/src/pages/index.astro
@@ -37,10 +37,10 @@ const agents = [
const setupPrompt = `Install and configure Artifact Server for me. Do the work, not only explain the steps.
Use these pages as the source of truth:
-- https://artifactserver.com/docs/get-started/
-- https://artifactserver.com/docs/deploy/
-- https://artifactserver.com/docs/mcp/
-- https://artifactserver.com/docs/agents/
+- https://artifactserver.com/docs/get-started/index.md
+- https://artifactserver.com/docs/deploy/index.md
+- https://artifactserver.com/docs/mcp/index.md
+- https://artifactserver.com/docs/agents/index.md
First, ask me exactly one question: “Do you want to run Artifact Server locally on this machine, or deploy it for a team?” Wait for my answer before you continue.
@@ -54,7 +54,7 @@ Then complete the matching setup:
- Verify the server health, the review application, the installed skill, and MCP tool discovery. Fix failures that are within the setup scope.
- Do not stop after producing a plan. Complete the setup unless you need a credential or a deployment choice from me.
-When the setup is complete, begin your final response with “Artifact Server is running at .” State whether it is local or deployed for a team, where its data is stored, whether the skill is installed, whether this agent is connected through MCP, and what you verified. Link to https://artifactserver.com/docs/ for operating guidance. If anything is incomplete, list the exact blocker instead of claiming success.`;
+When the setup is complete, begin your final response with “Artifact Server is running at .” State whether it is local or deployed for a team, where its data is stored, whether the skill is installed, whether this agent is connected through MCP, and what you verified. Link to https://artifactserver.com/docs/index.md for operating guidance. If anything is incomplete, list the exact blocker instead of claiming success.`;
---
From 2cab2b53a62273db3452be6602c64ad54db03a73 Mon Sep 17 00:00:00 2001
From: Michael Ramos
Date: Wed, 2 Sep 2026 12:18:13 -0700
Subject: [PATCH 3/5] fix(site): hide hero review mockup on mobile
---
apps/site/src/styles/landing.css | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/apps/site/src/styles/landing.css b/apps/site/src/styles/landing.css
index 1459885..193481c 100644
--- a/apps/site/src/styles/landing.css
+++ b/apps/site/src/styles/landing.css
@@ -1362,19 +1362,6 @@
}
.artifact-window {
- height: 30rem;
- box-shadow: 0.75rem 0.75rem 0 color-mix(in oklch, var(--nb-primary) 12%, transparent);
- }
-
- .artifact-window::before {
- display: none;
- }
-
- .artifact-window__body {
- grid-template-columns: 1fr;
- }
-
- .artifact-canvas {
display: none;
}
From 65d16f495fd504f8b72f709a84ed529067c396cd Mon Sep 17 00:00:00 2001
From: Michael Ramos
Date: Wed, 2 Sep 2026 12:19:18 -0700
Subject: [PATCH 4/5] fix(site): focus review card on team feedback
---
apps/site/src/pages/index.astro | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro
index 3ed93fd..76edf78 100644
--- a/apps/site/src/pages/index.astro
+++ b/apps/site/src/pages/index.astro
@@ -222,9 +222,9 @@ When the setup is complete, begin your final response with “Artifact Server is
03
-
Review and share feedback.
-
Comments stay attached to the exact version and selected content. Share a review link so the next change starts with the full context.
- Comment → Agent → New version
+
Review with your team.
+
Comments stay attached to the exact version and selected content. Share a review link so everyone can discuss the same work.
+ Review link → Team feedback → Next version
From 8bdb74233908746b488215b62c0c6afb74e64030 Mon Sep 17 00:00:00 2001
From: Michael Ramos
Date: Wed, 2 Sep 2026 12:49:27 -0700
Subject: [PATCH 5/5] docs(site): add deployment guides
---
apps/site/.nimbus/routes.json | 4 +
.../site/src/content/docs/docs/deploy/aws.mdx | 356 ++++++++++++++
.../content/docs/docs/deploy/cloudflare.mdx | 2 +
.../src/content/docs/docs/deploy/compose.mdx | 446 ++++++++++++++++++
.../content/docs/docs/deploy/google-cloud.mdx | 380 +++++++++++++++
.../src/content/docs/docs/deploy/index.mdx | 68 +--
.../content/docs/docs/deploy/kubernetes.mdx | 338 +++++++++++++
apps/site/src/content/docs/docs/index.mdx | 4 +
apps/site/src/pages/index.md.ts | 4 +
packaging/compose/README.md | 5 +-
project/spec/cloud-deployment-contract.md | 6 +-
11 files changed, 1573 insertions(+), 40 deletions(-)
create mode 100644 apps/site/src/content/docs/docs/deploy/aws.mdx
create mode 100644 apps/site/src/content/docs/docs/deploy/compose.mdx
create mode 100644 apps/site/src/content/docs/docs/deploy/google-cloud.mdx
create mode 100644 apps/site/src/content/docs/docs/deploy/kubernetes.mdx
diff --git a/apps/site/.nimbus/routes.json b/apps/site/.nimbus/routes.json
index 56c00c0..19c46d3 100644
--- a/apps/site/.nimbus/routes.json
+++ b/apps/site/.nimbus/routes.json
@@ -10,7 +10,11 @@
"/docs/concepts/security-boundary",
"/docs/connect-agents",
"/docs/deploy",
+ "/docs/deploy/aws",
"/docs/deploy/cloudflare",
+ "/docs/deploy/compose",
+ "/docs/deploy/google-cloud",
+ "/docs/deploy/kubernetes",
"/docs/get-started",
"/docs/mcp",
"/docs/publish",
diff --git a/apps/site/src/content/docs/docs/deploy/aws.mdx b/apps/site/src/content/docs/docs/deploy/aws.mdx
new file mode 100644
index 0000000..f487775
--- /dev/null
+++ b/apps/site/src/content/docs/docs/deploy/aws.mdx
@@ -0,0 +1,356 @@
+---
+title: Deploy on AWS
+description: Deploy the live-qualified public AWS stack with ECS Fargate, RDS PostgreSQL, S3, CloudFront, and Pulumi.
+sidebar:
+ order: 4
+---
+
+The AWS Pulumi package creates an Artifact Server installation on ECS Fargate, RDS PostgreSQL, S3, CloudFront, and Route 53.
+
+The package does not create an EKS cluster. Use the [Kubernetes guide](/docs/deploy/kubernetes/) for an existing EKS cluster.
+
+## Current qualification
+
+
+
+The live run covered clean deployment, repeat preview, product operations, two-task scaling, S3 outage recovery, state recovery, backup, and restore.
+
+The run also covered image upgrade, image rollback, secret rotation, 100 concurrent read users, and safe removal of the qualification stack.
+
+Private ingress has not passed the same live lifecycle. Treat private ingress as unqualified.
+
+The local `pnpm test:aws-pulumi` command verifies the resource graph and configuration rules. This command does not access live AWS resources.
+
+Read the [AWS findings](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/aws/FINDINGS.md) for the failures and corrections from the live run.
+
+## Deployment architecture
+
+| Product need | AWS service |
+| --- | --- |
+| Application runtime | ECS Fargate on Linux ARM64 |
+| Records | RDS PostgreSQL 17 |
+| Artifact and staged-upload files | Versioned S3 bucket |
+| Public HTTPS | CloudFront and an Application Load Balancer |
+| Certificates | AWS Certificate Manager (ACM) |
+| DNS | Route 53 |
+| Application credentials | Secrets Manager |
+| Workload identity | ECS task role |
+| Logs | CloudWatch Logs |
+| Staged-upload cleanup | EventBridge and a separate Fargate task |
+| Infrastructure lifecycle | Pulumi |
+
+The generated network puts the application in private subnets. RDS has no public address and accepts PostgreSQL traffic only from application tasks.
+
+For public ingress, CloudFront serves the application host and the wildcard content host. The load balancer accepts HTTPS only from CloudFront addresses.
+
+## Prerequisites
+
+Prepare these items before you create a stack:
+
+- AWS credentials that can create the services in the architecture table.
+- Node.js 24.12 or newer.
+- pnpm 10.34.3.
+- Pulumi CLI 3.257 or newer.
+- An existing Pulumi state backend or Pulumi Cloud organization.
+- An existing Pulumi secrets provider.
+- Two Route 53 hosted zones for separate registrable domains.
+- An OCI image digest that contains a Linux ARM64 image.
+- When you enable browser sign-in, prepare a WorkOS AuthKit client and secret ARN.
+
+
+
+The main stack never creates the storage for its Pulumi state. Enable encryption, versioning, access control, backup, and update locking on that backend.
+
+## Prepare the Pulumi stack
+
+
+
+ Run this command from the repository root.
+
+ ```sh
+ pnpm install --frozen-lockfile
+ ```
+
+
+ ```sh
+ cd deploy/pulumi/aws
+ ```
+
+
+ Replace the example URL with your existing backend.
+
+ ```sh
+ pulumi login s3://replace-with-existing-pulumi-state/artifact-server
+ ```
+
+
+ If the stack exists, select it.
+
+ ```sh
+ pulumi stack select production
+ ```
+
+ If the stack does not exist, create it with the selected secrets provider.
+
+ ```sh
+ pulumi stack init production \
+ --secrets-provider 'awskms://alias/artifact-server?region=us-east-1'
+ ```
+
+
+ ```sh
+ cp Pulumi.production.example.yaml Pulumi.production.yaml
+ ```
+
+
+
+## Configure the stack
+
+Edit `Pulumi.production.yaml`. Replace every example value.
+
+Use the [production configuration example](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/aws/Pulumi.production.example.yaml) as the complete key reference.
+
+| Configuration key | Rule |
+| --- | --- |
+| `applicationDomain` | Use the trusted application hostname. |
+| `contentDomain` | Use a separate registrable domain for untrusted version hosts. |
+| `imageReference` | Use an immutable `@sha256:` digest. |
+| `installationName` | Use 1 to 40 lowercase letters, numbers, or hyphens. |
+| `environment` | Use `development`, `staging`, or `production`. |
+| `aws:region` and `region` | Use the same AWS region in both keys. |
+| `capacity` | Set minimum tasks, maximum tasks, CPU, and memory. |
+| `databasePlan` | Use `small`, `standard`, or `high-availability`. |
+| `backupRetentionDays` | Use 7 to 35 days. Production requires at least 14 days. |
+| `deletionProtection` | Use `true` for production. |
+| `dnsZoneIds` | Supply different Route 53 zones for the application and content domains. |
+| `stateBackendUrl` | Record the active Pulumi backend address. |
+| `secretsProvider` | Record the active Pulumi secrets provider. |
+| `stackName` | Match the active Pulumi stack name. |
+| `workosClientId`, `workosIssuer`, `workosApiKeySecretRef` | Supply all three values, or omit all three values. |
+
+The package rejects unsafe CPU and memory combinations. It also rejects task counts that exceed the selected RDS connection budget.
+
+### Use the generated network
+
+The default network spans two availability zones. It contains public load-balancer subnets, private application subnets, and isolated database subnets.
+
+Production creates two NAT gateways. Development and staging create one NAT gateway.
+
+### Use an existing VPC
+
+Set every value in `existingNetwork`:
+
+```yaml
+artifact-server-aws:existingNetwork:
+ vpcId: vpc-replace
+ loadBalancerSubnetIds: [subnet-lb-a, subnet-lb-b]
+ applicationSubnetIds: [subnet-app-a, subnet-app-b]
+ databaseSubnetIds: [subnet-db-a, subnet-db-b]
+```
+
+Each subnet group must belong to the VPC. Each subnet group must span at least two availability zones.
+
+Application subnets need outbound access to the OCI registry, S3, Secrets Manager, and the configured identity service.
+
+If you configure OTLP export, application subnets also need access to the telemetry endpoint.
+
+Database subnets must not have public routes.
+
+## Configure HTTPS and DNS
+
+### Public ingress
+
+Set `ingress: public`. Supply both Route 53 zone IDs.
+
+Pulumi creates regional ACM certificates for the load balancer. It also creates a CloudFront certificate in `us-east-1`.
+
+Pulumi creates Route 53 aliases for the application hostname and `*.contentDomain`. Both aliases point to CloudFront.
+
+CloudFront uses the origin `Cache-Control` headers. Private application and API responses use `private, no-store` and are not cached.
+
+### Private ingress
+
+
+
+Set `ingress: private`. Set `tlsCertificateArn` to an existing ACM certificate that covers both hostnames.
+
+The private stack creates an internal load balancer and no CloudFront distribution.
+
+If Pulumi manages private DNS, supply both `dnsZoneIds` values. Otherwise, create the private DNS records outside this stack.
+
+By default, private HTTPS accepts the VPC CIDR. Set `privateIngressCidrs` for other trusted IPv4 networks.
+
+## Configure identity and secrets
+
+Store the WorkOS API key in Secrets Manager before deployment. Put only its ARN in `workosApiKeySecretRef`.
+
+The stack creates the Artifact Server API credential and database URL. It stores both values in Secrets Manager.
+
+ECS injects these values into the container. Pulumi outputs contain only secret resource identifiers.
+
+The ECS task role uses temporary AWS credentials. Its S3 policy covers only the installation prefix.
+
+The package exposes typed WorkOS configuration. It does not expose typed configuration for a generic OIDC provider.
+
+## Preview and deploy
+
+
+
+ Run this command from the repository root.
+
+ ```sh
+ pnpm test:aws-pulumi
+ ```
+
+
+ Run this command from `deploy/pulumi/aws`.
+
+ ```sh
+ pulumi preview --stack production
+ ```
+
+ Verify the expected resources. Verify that no generated credential appears in the preview.
+
+
+ ```sh
+ pulumi up --stack production
+ ```
+
+
+ ```sh
+ pulumi stack output deployment --stack production --json
+ ```
+
+ Keep this secret-free record with the release evidence.
+
+
+
+Each Fargate task applies compatible database migrations before the server starts. A PostgreSQL advisory lock serializes concurrent migration attempts.
+
+The ECS deployment circuit breaker rolls back tasks that do not become ready.
+
+## Verify the deployment
+
+
+
+ ```sh
+ application_url=$(pulumi stack output applicationUrl --stack production)
+ ```
+
+
+ ```sh
+ curl --fail --silent "$application_url/health"
+ ```
+
+
+ ```sh
+ curl --fail --silent "$application_url/ready"
+ ```
+
+
+ Open the application URL. Sign in with the bootstrap administrator email.
+
+
+ Publish one artifact. Open its exact-version Review link.
+
+
+ [Connect an agent to the MCP endpoint](/docs/mcp/). Verify tool discovery and an authenticated read.
+
+
+
+The deployment record also contains the health URL, readiness URL, MCP URL, log destination, and support manifest location.
+
+## Back up and restore
+
+The stack configures RDS automated backups and an RDS final snapshot. It also enables S3 versioning and blocks public bucket access.
+
+The stack does not create a coordinated backup service. A usable recovery point must contain matching RDS and S3 states.
+
+Create a production restore procedure with these operations:
+
+1. Suspend ECS autoscaling.
+2. Stop all application tasks.
+3. Create an RDS snapshot.
+4. Copy the current S3 objects to a separate protected bucket.
+5. Restore the original ECS capacity.
+6. Restore the snapshot into a clean RDS instance.
+7. Restore the object copy into a clean S3 bucket.
+8. Run `artifactserver integrity check --mode external-storage` against the restored providers.
+9. Verify the installation, project, artifact, version, and file identities.
+10. Remove the temporary restore resources after the verification passes.
+
+Do not point an existing database at an unrelated S3 namespace. Preserve the installation identifier and the matching database and file set.
+
+The [tested AWS restore workflow](https://github.com/plannotator/artifact-server/blob/main/scripts/run-aws-coordinated-restore-qualification.sh) is qualification evidence. Adapt it to your recovery controls before production use.
+
+Back up the Pulumi state backend separately. The live qualification restored an exact checkpoint from a versioned S3 backend.
+
+## Update and roll back
+
+Record the current image digest before an update. Create a coordinated backup before a database migration.
+
+
+
+ Use an immutable digest that contains a Linux ARM64 image.
+
+ ```sh
+ pulumi config set imageReference \
+ 'repository@sha256:replace_with_64_hex_characters' \
+ --stack production
+ ```
+
+
+ ```sh
+ pulumi preview --stack production
+ ```
+
+
+ ```sh
+ pulumi up --stack production
+ ```
+
+
+ Verify health, readiness, browser access, artifact delivery, and MCP access.
+
+
+
+For rollback, set `imageReference` to the previous digest. Then preview and apply the stack again.
+
+Use only a release that supports the current database schema. The live qualification covered one schema-compatible update and rollback.
+
+## Remove a stack
+
+
+
+The S3 bucket uses `forceDestroy: false`. Pulumi cannot delete a nonempty bucket.
+
+Before permanent removal, verify a coordinated backup and a clean restore. Remove protection only through a separately approved procedure.
+
+For an approved, unprotected, empty non-production stack, run:
+
+```sh
+pulumi destroy --stack staging
+```
+
+RDS creates a final snapshot during removal. Verify that the stack has no remaining managed resources.
+
+## Known limits
+
+- Only public ingress has passed the live AWS lifecycle.
+- The stack uses ARM64 Fargate tasks.
+- The stack does not create EKS.
+- The stack does not create its Pulumi state backend.
+- The stack does not automate coordinated RDS and S3 backups.
+- The Pulumi package exposes WorkOS inputs but no typed generic OIDC inputs.
+- The first public Artifact Server image is not available yet.
+- Complete signed release evidence and full product conformance remain open.
+
+Read the [AWS package guide](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/aws/README.md) for the source-level resource details and qualification commands.
diff --git a/apps/site/src/content/docs/docs/deploy/cloudflare.mdx b/apps/site/src/content/docs/docs/deploy/cloudflare.mdx
index 4197430..815e72e 100644
--- a/apps/site/src/content/docs/docs/deploy/cloudflare.mdx
+++ b/apps/site/src/content/docs/docs/deploy/cloudflare.mdx
@@ -1,6 +1,8 @@
---
title: Deploy on Cloudflare
description: The live-qualified Artifact Server deployment path that uses Workers, D1, R2, and Alchemy.
+sidebar:
+ order: 1
---
Cloudflare is Artifact Server's live-qualified direct-cloud target. The deployment package runs the same product services on Cloudflare infrastructure.
diff --git a/apps/site/src/content/docs/docs/deploy/compose.mdx b/apps/site/src/content/docs/docs/deploy/compose.mdx
new file mode 100644
index 0000000..6a4f5f9
--- /dev/null
+++ b/apps/site/src/content/docs/docs/deploy/compose.mdx
@@ -0,0 +1,446 @@
+---
+title: Deploy with Compose
+description: Run Artifact Server on one server with compact or external storage.
+sidebar:
+ order: 2
+---
+
+Compose provides two team deployment modes. Both modes use the same OCI image and private HTTP service.
+
+| Mode | Application processes | Records | Artifact files | Use this mode when |
+| --- | --- | --- | --- | --- |
+| Compact Compose | Exactly one | SQLite | One file volume | One server owns all durable data. |
+| External-storage Compose | One or more | Existing PostgreSQL | Existing S3-compatible storage | Your team operates the durable providers separately. |
+
+
+
+
+
+## Choose a mode
+
+Use Compact Compose for the shortest one-server team installation. This mode does not provide application failover or horizontal scaling.
+
+Use external-storage Compose for multiple application processes. This mode requires PostgreSQL and supported S3-compatible storage before startup.
+
+Artifact Server does not implement transfer from compact storage to external storage. Select the durable storage model before production use.
+
+## Meet the shared requirements
+
+Both modes require these items:
+
+- Docker with Docker Compose.
+- An immutable Artifact Server image digest for the release.
+- One HTTPS application origin, such as `https://artifacts.example.com`.
+- One separate registrable content domain, such as `content.example.net`.
+- Wildcard DNS and TLS for `*.content.example.net`.
+- One OIDC or WorkOS browser-login provider.
+- A trusted reverse proxy or gateway.
+
+The package does not install DNS, certificates, a reverse proxy, PostgreSQL, or object storage.
+
+## Prepare the server
+
+Copy the Compose package from the source checkout. Then work from the copied directory.
+
+```sh
+cp -R packaging/compose /srv/artifact-server
+cd /srv/artifact-server
+cp .env.example .env
+```
+
+Edit `.env`. Set the shared configuration:
+
+```dotenv
+ARTIFACT_SERVER_IMAGE=ghcr.io/plannotator/artifact-server@sha256:replace_with_release_digest
+ARTIFACT_SERVER_BIND_ADDRESS=127.0.0.1
+ARTIFACT_SERVER_PORT=8787
+ARTIFACT_SERVER_ORIGIN=https://artifacts.example.com
+ARTIFACT_SERVER_CONTENT_DOMAIN=content.example.net
+ARTIFACT_SERVER_READINESS_WITHDRAWAL_MS=1000
+ARTIFACT_SERVER_SHUTDOWN_DEADLINE_MS=10000
+ARTIFACT_SERVER_REQUEST_LOG_SAMPLE_RATE=0.01
+```
+
+The application origin and content domain must have different registrable domains. This rule keeps untrusted artifact content outside the trusted application origin.
+
+[Read the security boundary →](/docs/concepts/security-boundary/)
+
+## Configure browser login
+
+Configure exactly one browser-login provider. A partial configuration or two provider families cause startup to fail.
+
+### OIDC
+
+Register this redirect URI with the OIDC provider:
+
+```text
+https://artifacts.example.com/auth/callback
+```
+
+Set these values in `.env`:
+
+```dotenv
+ARTIFACT_SERVER_OIDC_ISSUER=https://idp.example.com/realms/main
+ARTIFACT_SERVER_OIDC_CLIENT_ID=artifact-server
+ARTIFACT_SERVER_OIDC_SCOPES=openid email profile
+```
+
+The client secret is optional for a public client that uses PKCE. A confidential client requires one secret source.
+
+Use `ARTIFACT_SERVER_OIDC_CLIENT_SECRET_FILE` for a mounted secret file. Add a private Compose override that mounts this file read-only.
+
+### WorkOS
+
+Set the exact AuthKit issuer and client ID:
+
+```dotenv
+ARTIFACT_SERVER_WORKOS_ISSUER=https://replace.authkit.app
+ARTIFACT_SERVER_WORKOS_CLIENT_ID=client_replace_me
+```
+
+WorkOS also requires `ARTIFACT_SERVER_WORKOS_API_KEY_FILE`. Add a private Compose override that mounts this file read-only.
+
+Use one dedicated WorkOS environment for each Artifact Server environment.
+
+## Configure the reverse proxy
+
+Route the application hostname and all wildcard content hosts to port 8787. Preserve the requested host and HTTPS protocol.
+
+The proxy must support streamed uploads, response streaming, required timeouts, and the configured upload limits.
+
+Keep the default loopback bind for one replica behind a host-level proxy. Attach a network gateway for multiple external-storage replicas.
+
+Caddy, Nginx, Traefik, HAProxy, or an existing gateway can provide this boundary. The Compose package does not select one.
+
+Public artifact access removes the Artifact Server login check. It does not make a private server reachable from the internet.
+
+## Run Compact Compose
+
+Compact Compose stores SQLite, artifact files, staged uploads, and generated secrets in the `artifact-server-data` volume.
+
+Verify that the server has space for the active volume, one backup copy, and expected growth.
+
+
+
+ ```sh
+ docker compose config
+ ```
+
+
+ ```sh
+ docker compose run --rm --no-deps artifact-server \
+ init --admin-email admin@example.com \
+ --data /var/lib/artifact-server/data
+ ```
+
+ The command creates the installation identity and machine API credential. It prints only the installation ID and data directory.
+
+
+ ```sh
+ docker compose up --detach --wait
+ ```
+
+
+ ```sh
+ curl --fail http://127.0.0.1:8787/health
+ curl --fail http://127.0.0.1:8787/ready
+ ```
+
+ Then open the configured HTTPS application origin through the reverse proxy.
+
+
+
+Do not scale the compact service. The fixed container name makes a second writer fail during Compose startup.
+
+The volume needs reliable SQLite locking and atomic replacement. NFS-like filesystems do not have qualification for this mode.
+
+### Back up Compact Compose
+
+Run the included backup script with a new target directory:
+
+```sh
+./compact-backup.sh /srv/backups/artifact-server/2026-09-02
+```
+
+The script stops the service before it copies data. Then it writes these files:
+
+- `data.tar`
+- `data.tar.sha256`
+- `support-manifest.json`
+
+The script restarts the service after a successful copy. A failed backup retains an `INCOMPLETE` marker.
+
+### Restore Compact Compose
+
+Stop the target service. Use an empty target volume and a new Compose project name for a recovery drill.
+
+```sh
+export COMPOSE_PROJECT_NAME=artifact-server-restored
+./compact-restore.sh /srv/backups/artifact-server/2026-09-02
+docker compose up --detach --wait
+```
+
+The restore script verifies the checksum, archive paths, filesystem entries, and complete installation integrity.
+
+The script rejects running targets, nonempty volumes, incomplete backups, unsafe paths, links, special files, and checksum failures.
+
+If you restore the same installation, keep the application origin and content domain unchanged.
+
+Verify the installation ID, projects, artifacts, versions, actions, access configuration, manifests, and exact file bytes.
+
+## Run external-storage Compose
+
+External-storage Compose uses the base file and `compose.external-storage.yaml`. The override removes the compact volume and fixed container name.
+
+Provide these dependencies before startup:
+
+- One PostgreSQL database for this installation.
+- One AWS S3 or Cloudflare R2 bucket.
+- One gateway on the Compose network for multiple replicas.
+- Enough database connections for every replica and migration process.
+
+Another S3-compatible provider requires successful Artifact Server adapter contract tests. Provider compatibility text alone is not qualification.
+
+MinIO is an integration-test provider. The production package does not install or recommend MinIO.
+
+### Create the required secret files
+
+Create the secret directory and machine API credential:
+
+```sh
+install -d -o 1000 -g 1000 -m 0700 /etc/artifact-server
+printf 'as_key_key_%s_%s\n' "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" \
+ > /etc/artifact-server/api-token
+chown 1000:1000 /etc/artifact-server/api-token
+chmod 0400 /etc/artifact-server/api-token
+```
+
+Write the complete PostgreSQL URL to its secret file:
+
+```sh
+printf '%s\n' 'postgresql://artifactserver:replace_me@postgres.example/artifactserver' \
+ > /etc/artifact-server/database-url
+chown 1000:1000 /etc/artifact-server/database-url
+chmod 0400 /etc/artifact-server/database-url
+```
+
+The image runs with UID and GID `1000:1000`. Each mounted secret file must use this ownership and mode `0400`.
+
+Do not put the machine API credential or PostgreSQL URL in `.env`.
+
+### Configure the external providers
+
+Add these values to `.env`:
+
+```dotenv
+ARTIFACT_SERVER_INSTALLATION_ID=artifact-server-production
+ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
+ARTIFACT_SERVER_API_TOKEN_SECRET_FILE=/etc/artifact-server/api-token
+ARTIFACT_SERVER_DATABASE_URL_SECRET_FILE=/etc/artifact-server/database-url
+ARTIFACT_SERVER_POSTGRES_MAX_CONNECTIONS=10
+ARTIFACT_SERVER_S3_BUCKET=replace-me
+ARTIFACT_SERVER_S3_REGION=us-east-1
+ARTIFACT_SERVER_S3_FORCE_PATH_STYLE=false
+```
+
+Keep `ARTIFACT_SERVER_INSTALLATION_ID` stable for the complete installation lifetime.
+
+AWS installations can use an instance role or another AWS workload identity. Do not add static S3 keys for that configuration.
+
+For R2 or another qualified provider, copy the static-credential overlay:
+
+```sh
+cp compose.external-storage.s3-credentials.yaml.example \
+ compose.external-storage.s3-credentials.yaml
+```
+
+Create both S3 credential files with UID and GID `1000:1000`. Set each file mode to `0400`.
+
+Set these configuration values for the private overlay:
+
+```dotenv
+ARTIFACT_SERVER_S3_ENDPOINT=https://replace-me.r2.cloudflarestorage.com
+ARTIFACT_SERVER_S3_ACCESS_KEY_ID_SECRET_FILE=/etc/artifact-server/s3-access-key-id
+ARTIFACT_SERVER_S3_SECRET_ACCESS_KEY_SECRET_FILE=/etc/artifact-server/s3-secret-access-key
+```
+
+If you use static credentials, add `--file compose.external-storage.s3-credentials.yaml` to every external-storage command.
+
+### Verify the configuration and migrate PostgreSQL
+
+Parse the complete configuration and connect to both providers:
+
+```sh
+docker compose --file compose.yaml --file compose.external-storage.yaml \
+ run --rm --no-deps artifact-server \
+ config check --mode external-storage
+```
+
+Verify the current schema:
+
+```sh
+docker compose --file compose.yaml --file compose.external-storage.yaml \
+ run --rm --no-deps artifact-server migrate status
+```
+
+Apply the release migrations:
+
+```sh
+docker compose --file compose.yaml --file compose.external-storage.yaml \
+ run --rm --no-deps artifact-server migrate apply
+```
+
+Serving containers verify the schema. They never apply migrations during startup.
+
+### Start one replica
+
+Use the loopback overlay for one replica behind a host-level reverse proxy:
+
+```sh
+docker compose --file compose.yaml \
+ --file compose.external-storage.yaml \
+ --file compose.external-storage.local-port.yaml \
+ up --detach --wait
+```
+
+Verify the local process:
+
+```sh
+curl --fail http://127.0.0.1:8787/health
+curl --fail http://127.0.0.1:8787/ready
+```
+
+Do not combine the fixed local-port overlay with multiple replicas.
+
+### Start multiple replicas
+
+Attach the reverse proxy or gateway to the Compose network. Then start the replicas:
+
+```sh
+docker compose --file compose.yaml --file compose.external-storage.yaml \
+ up --detach --scale artifact-server=2 --wait
+```
+
+Route both HTTPS host families to the `artifact-server` service on port 8787.
+
+Set `ARTIFACT_SERVER_POSTGRES_MAX_CONNECTIONS` from the PostgreSQL connection budget. Each replica uses its own bounded pool.
+
+The package does not configure autoscaling, load balancing, or multi-region failover.
+
+### Back up external storage
+
+PostgreSQL and the complete Artifact Server object prefix form one backup unit. A database-only or bucket-only backup is incomplete.
+
+
+
+ ```sh
+ docker compose --file compose.yaml --file compose.external-storage.yaml \
+ stop artifact-server
+ ```
+
+
+ Create one transaction-consistent logical PostgreSQL backup with the provider procedure.
+
+
+ Copy the complete Artifact Server installation prefix with the provider procedure.
+
+
+ ```sh
+ docker compose --file compose.yaml --file compose.external-storage.yaml \
+ run --rm --no-deps artifact-server \
+ support manifest --mode external-storage
+ ```
+
+ Store this output and checksums with both backup parts.
+
+
+ Restore both parts into empty providers. Then run the integrity command:
+
+ ```sh
+ docker compose --file compose.yaml --file compose.external-storage.yaml \
+ run --rm --no-deps artifact-server \
+ integrity check --mode external-storage
+ ```
+
+
+
+Start the application only after the integrity report has status `healthy`.
+
+## Update the application image
+
+Cross-version update and rollback compatibility does not have a release qualification yet. Do not change versions without release-specific compatibility instructions.
+
+When release instructions permit an update, use this sequence:
+
+
+
+ Use the backup procedure for the selected storage mode.
+
+
+ Replace `ARTIFACT_SERVER_IMAGE` with the exact digest from the release instructions.
+
+
+ Run `docker compose config` for compact mode. Run `config check` for external-storage mode.
+
+
+ For external-storage mode, run `migrate status`. Then run `migrate apply`.
+
+
+ Run `docker compose up --detach --force-recreate --wait` with the files for the selected mode.
+
+
+ Verify `/health`, `/ready`, browser sign-in, one private artifact, and one public artifact.
+
+
+
+The prior image must accept the current schema before rollback. Artifact Server does not apply an automatic down-migration.
+
+## Remove the Compose application
+
+Stop and remove compact application containers while you preserve the data volume:
+
+```sh
+docker compose down
+```
+
+
+
+Stop and remove external-storage application containers with both Compose files:
+
+```sh
+docker compose --file compose.yaml --file compose.external-storage.yaml down
+```
+
+The external command does not remove PostgreSQL or object storage. Those providers are outside the Compose package.
+
+## Known limits
+
+- Compact Compose supports exactly one application process.
+- Compact Compose does not support NFS-like data volumes without separate qualification.
+- External-storage Compose does not install or back up its durable providers.
+- External-storage Compose does not configure autoscaling or multi-region failover.
+- The package does not configure a reverse proxy, DNS, TLS, or firewall rules.
+- Cross-version update and rollback compatibility does not have qualification.
+- Compact-to-external-storage transfer is not implemented.
+
+## Read the package sources
+
+- [Compose package instructions](https://github.com/plannotator/artifact-server/blob/main/packaging/compose/README.md)
+- [Compact Compose file](https://github.com/plannotator/artifact-server/blob/main/packaging/compose/compose.yaml)
+- [External-storage Compose file](https://github.com/plannotator/artifact-server/blob/main/packaging/compose/compose.external-storage.yaml)
+- [Compact recovery decision](https://github.com/plannotator/artifact-server/blob/main/project/spec/decisions/0011-compact-compose-recovery.md)
+- [External-storage decision](https://github.com/plannotator/artifact-server/blob/main/project/spec/decisions/0012-external-storage-compose.md)
+- [Compact release test](https://github.com/plannotator/artifact-server/blob/main/tests/release/compact-compose.test.ts)
+- [External-storage release test](https://github.com/plannotator/artifact-server/blob/main/tests/release/external-storage-compose.test.ts)
diff --git a/apps/site/src/content/docs/docs/deploy/google-cloud.mdx b/apps/site/src/content/docs/docs/deploy/google-cloud.mdx
new file mode 100644
index 0000000..38c05e2
--- /dev/null
+++ b/apps/site/src/content/docs/docs/deploy/google-cloud.mdx
@@ -0,0 +1,380 @@
+---
+title: Deploy on Google Cloud
+description: Deploy Artifact Server on Cloud Run, Cloud SQL for PostgreSQL, and Google Cloud Storage with the checked-in Pulumi project.
+sidebar:
+ order: 5
+---
+
+The Google Cloud package deploys the external-storage runtime with Pulumi. It creates Cloud Run, Cloud SQL, Google Cloud Storage, DNS, TLS, and edge resources.
+
+
+
+The live run covered product operations, bounded reads, upgrades, rollback, API-secret rotation, state import, provider restore operations, and safe deletion. The release ledger still requires complete signed evidence and full product conformance.
+
+## Deployment architecture
+
+| Product need | Google Cloud service |
+| --- | --- |
+| Application runtime | Cloud Run |
+| Durable records | Cloud SQL for PostgreSQL 17 |
+| Artifact and staged-upload bytes | Google Cloud Storage |
+| Public edge | External Application Load Balancer and Cloud CDN |
+| DNS and TLS | Cloud DNS and Certificate Manager |
+| Application credentials | Secret Manager |
+| Workload identity | Dedicated Cloud Run service account |
+| Expired-upload cleanup | Cloud Run Job and Cloud Scheduler |
+| Provisioning and lifecycle | Pulumi |
+
+Cloud Run accepts traffic from the load balancer. The package disables the default Cloud Run URL.
+
+The load balancer serves the application host and wildcard content hosts. Cloud CDN follows origin cache headers and bypasses every request with a `Range` header.
+
+Cloud SQL uses private IP. The storage bucket blocks public access and uses uniform bucket access.
+
+## Prerequisites
+
+Prepare these items before you create a stack:
+
+- A dedicated Google Cloud project.
+- A source checkout with Node.js 24.12 or later and pnpm 10.34.3.
+- The Pulumi CLI and Google Cloud CLI.
+- Application Default Credentials for the Pulumi process.
+- An existing versioned GCS bucket or Pulumi Cloud for Pulumi state.
+- A Pulumi secrets provider, such as one Cloud KMS key.
+- A multi-architecture Artifact Server image pinned to one SHA-256 digest.
+- Two separate registrable domains.
+- One authoritative Cloud DNS managed zone for each domain.
+
+The Pulumi identity must manage Service Usage, IAM, Cloud Run, Cloud SQL, GCS, Compute, DNS, Certificate Manager, Secret Manager, and Cloud Scheduler.
+
+The identity must also change records in both existing Cloud DNS managed zones. The repository does not define a minimum operator IAM role.
+
+The stack enables these Google Cloud APIs:
+
+```text
+certificatemanager.googleapis.com
+cloudscheduler.googleapis.com
+compute.googleapis.com
+dns.googleapis.com
+run.googleapis.com
+secretmanager.googleapis.com
+servicenetworking.googleapis.com
+sqladmin.googleapis.com
+storage.googleapis.com
+```
+
+If you use Cloud KMS for Pulumi secrets, enable `cloudkms.googleapis.com` before you create the stack.
+
+## Prepare Pulumi
+
+Install the repository dependencies:
+
+```sh
+pnpm install --frozen-lockfile
+```
+
+Authenticate Pulumi through Application Default Credentials. For an interactive shell, run these commands:
+
+```sh
+gcloud auth application-default login
+gcloud config set project YOUR_GCP_PROJECT_ID
+```
+
+Select the external state backend:
+
+```sh
+pulumi login gs://YOUR_STATE_BUCKET/artifact-server
+```
+
+Create the stack with its secrets provider:
+
+```sh
+pulumi stack init production \
+ --cwd deploy/pulumi/gcp \
+ --secrets-provider gcpkms://projects/YOUR_GCP_PROJECT_ID/locations/global/keyRings/YOUR_KEY_RING/cryptoKeys/YOUR_KEY
+```
+
+If the stack exists, select it instead:
+
+```sh
+pulumi stack select production --cwd deploy/pulumi/gcp
+```
+
+Enable object versioning on the state bucket. The checked-in state recovery probe rejects a GCS backend without versioning.
+
+## Configure the stack
+
+Create `deploy/pulumi/gcp/Pulumi.production.yaml`. Replace every example value before you run a preview.
+
+```yaml
+config:
+ gcp:project: YOUR_GCP_PROJECT_ID
+ gcp:region: us-central1
+ artifact-server-gcp:applicationDomain: artifacts.example.com
+ artifact-server-gcp:backupRetentionDays: 14
+ artifact-server-gcp:bootstrapAdministratorEmail: admin@example.com
+ artifact-server-gcp:capacity:
+ cpu: 1
+ maximumInstances: 3
+ memoryMiB: 2048
+ minimumInstances: 2
+ artifact-server-gcp:contentDomain: artifact-content.example.net
+ artifact-server-gcp:databasePlan: high-availability
+ artifact-server-gcp:deletionProtection: true
+ artifact-server-gcp:dnsZoneIds:
+ application: artifacts-example-com
+ content: artifact-content-example-net
+ artifact-server-gcp:environment: production
+ artifact-server-gcp:imageReference: ghcr.io/plannotator/artifact-server@sha256:REPLACE_WITH_64_HEX_CHARACTERS
+ artifact-server-gcp:ingress: public
+ artifact-server-gcp:installationName: artifact-server
+ artifact-server-gcp:region: us-central1
+ artifact-server-gcp:secretsProvider: gcpkms://projects/YOUR_GCP_PROJECT_ID/locations/global/keyRings/YOUR_KEY_RING/cryptoKeys/YOUR_KEY
+ artifact-server-gcp:stackName: production
+ artifact-server-gcp:stateBackendUrl: gs://YOUR_STATE_BUCKET/artifact-server
+```
+
+`dnsZoneIds.application` and `dnsZoneIds.content` are managed-zone names. They are not project numbers or DNS zone numbers.
+
+The application and content domains must have different registrable domains. Public ingress requires both DNS zone values.
+
+Production configuration requires these values:
+
+- `minimumInstances` is 1 or more.
+- `backupRetentionDays` is 14 through 35.
+- `deletionProtection` is `true`.
+- `imageReference` contains one immutable SHA-256 digest.
+
+Optional configuration includes `otlpEndpoint`, `requestLogSampleRate`, and `resourceTags`. The request-log sample rate defaults to `0.01`.
+
+The GCP package accepts 0.25, 0.5, 1, 2, 4, or 8 CPUs. Memory must be 512 MiB through 32,768 MiB.
+
+| Database plan | Cloud SQL availability | Tier | Initial disk | Connection budget |
+| --- | --- | --- | --- | --- |
+| `small` | Zonal | `db-custom-1-3840` | 20 GiB | 80 |
+| `standard` | Zonal | `db-custom-2-7680` | 50 GiB | 200 |
+| `high-availability` | Regional | `db-custom-4-15360` | 100 GiB | 500 |
+
+The package reserves connections for a rolling replacement. It rejects a maximum instance count that exceeds the selected database budget.
+
+### Use an existing network
+
+The package creates a VPC, a `/24` regional subnet, and private service networking by default.
+
+Set `existingNetwork` to use customer-owned network resources:
+
+```yaml
+ artifact-server-gcp:existingNetwork:
+ privateServiceConnection: projects/YOUR_GCP_PROJECT_ID/global/networks/YOUR_VPC/peerings/YOUR_SQL_PEERING
+ vpcEgressConfiguration: projects/YOUR_GCP_PROJECT_ID/regions/us-central1/subnetworks/YOUR_SUBNET
+ vpcName: projects/YOUR_GCP_PROJECT_ID/global/networks/YOUR_VPC
+```
+
+Make sure that private services access works before deployment. Reserve enough subnet addresses for the maximum Cloud Run instance count.
+
+## Configure authentication and secrets
+
+The stack generates an API token and database password. It stores both values in Secret Manager and omits them from stack outputs.
+
+The Cloud Run service account receives access to generated secrets, the artifact bucket, and Cloud SQL. It uses Application Default Credentials for GCS.
+
+The direct GCP package exposes typed WorkOS configuration. It does not expose typed generic OIDC configuration.
+
+The stack can start without browser login configuration. Do not treat that mode as a team-ready installation.
+
+To enable WorkOS, set all three values together:
+
+```yaml
+ artifact-server-gcp:workosApiKeySecretRef: YOUR_WORKOS_SECRET_ID
+ artifact-server-gcp:workosClientId: client_YOUR_CLIENT_ID
+ artifact-server-gcp:workosIssuer: https://YOUR_TENANT.authkit.app
+```
+
+`workosApiKeySecretRef` identifies an existing Secret Manager secret. It does not contain the WorkOS API key.
+
+Grant the generated Cloud Run service account `roles/secretmanager.secretAccessor` on this secret. The Pulumi project does not create that grant.
+
+## Preview and deploy
+
+Run the package verification before you create cloud resources:
+
+```sh
+pnpm --dir deploy/pulumi/gcp verify
+```
+
+Review the infrastructure preview:
+
+```sh
+pulumi preview --cwd deploy/pulumi/gcp --stack production
+```
+
+Create or update the stack:
+
+```sh
+pulumi up --cwd deploy/pulumi/gcp --stack production
+```
+
+Certificate Manager creates DNS authorization records for both domains. Certificate issuance and DNS changes can delay the first healthy response.
+
+## Verify the deployment
+
+Save the secret-free deployment output:
+
+```sh
+pulumi stack output deployment \
+ --cwd deploy/pulumi/gcp \
+ --stack production \
+ --json > deployment.json
+```
+
+Verify the health and readiness endpoints:
+
+```sh
+curl --fail --silent --show-error "$(jq -r '.healthUrl' deployment.json)"
+curl --fail --silent --show-error "$(jq -r '.readinessUrl' deployment.json)"
+```
+
+Open the `applicationUrl` from `deployment.json`. If you configured WorkOS, verify browser sign-in.
+
+Verify publication, private access, version reads, comments, and MCP discovery.
+
+The output also contains the runtime, database, bucket, network, secret, state, identity, log, and support-manifest resource identifiers.
+
+
+
+Run the repeatable product probe against an isolated stack:
+
+```sh
+PULUMI_BACKEND_URL=gs://YOUR_STATE_BUCKET/artifact-server \
+ARTIFACT_SERVER_GCP_QUALIFICATION_STACK=gcp-qualification \
+ scripts/run-gcp-deployment-product-qualification.sh
+```
+
+## Back up and restore
+
+Cloud SQL keeps scheduled backups and point-in-time recovery. The selected plan retains 7 through 35 backups.
+
+The GCS bucket enables object versioning and soft deletion. Its soft-delete period equals `backupRetentionDays`.
+
+These independent controls do not form one coordinated recovery point. Quiesce writes before you back up both data stores.
+
+Create one Cloud SQL backup. Then copy the complete artifact bucket. Record both provider results as one recovery set.
+
+Restore the database into an empty private instance. Then restore the objects into an empty private bucket.
+
+Verify installation IDs, project IDs, artifact IDs, version IDs, names, sizes, and checksums.
+
+The live qualification restored Cloud SQL and copied 29 GCS objects with exact names, sizes, and CRC32C checksums. A later restored installation passed the integrity scanner.
+
+The repository does not provide automated disaster recovery or cross-region failover. The release ledger still requires detailed application-level restore evidence.
+
+## Update and roll back
+
+Create one coordinated recovery set before you update the stack.
+
+Record the current image digest before an update:
+
+```sh
+pulumi config get imageReference \
+ --cwd deploy/pulumi/gcp \
+ --stack production
+```
+
+Set the new digest-pinned image:
+
+```sh
+pulumi config set imageReference REGISTRY/IMAGE@sha256:NEW_DIGEST \
+ --cwd deploy/pulumi/gcp \
+ --stack production
+```
+
+Preview the update. Then apply it:
+
+```sh
+pulumi preview --cwd deploy/pulumi/gcp --stack production
+pulumi up --cwd deploy/pulumi/gcp --stack production
+```
+
+Verify the product after the update. The container applies database migrations before it starts the server.
+
+If the image is schema-compatible, set the prior digest. Then run `pulumi up` again.
+
+Do not use an incompatible image for rollback.
+
+The live qualification preserved the installation, database, and bucket identities during its update and rollback.
+
+## Recover Pulumi state
+
+Keep the GCS state bucket outside this stack. Enable object versioning.
+
+Protect the bucket with its own access controls.
+
+Pulumi state recovery depends on the selected backend. Follow the Pulumi recovery procedure for that backend.
+
+The checked-in probe exports and imports one exact checkpoint. It then compares resource identities and requires a no-change preview.
+
+Run this probe only against an isolated stack:
+
+```sh
+PULUMI_BACKEND_URL=gs://YOUR_STATE_BUCKET/artifact-server \
+ARTIFACT_SERVER_GCP_QUALIFICATION_STACK=gcp-qualification \
+ scripts/run-gcp-state-recovery-qualification.sh
+```
+
+## Delete a stack
+
+
+
+Production configuration requires deletion protection. The package does not include a procedure that disables production protection.
+
+For an unprotected non-production stack, run the native destroy command:
+
+```sh
+pulumi destroy \
+ --cwd deploy/pulumi/gcp \
+ --stack staging \
+ --yes \
+ --non-interactive
+```
+
+If the artifact bucket stops deletion, back up its contents. Then remove every object version.
+
+Run the same destroy command again.
+
+Cloud Run can retain subnet addresses for one or two hours. If an address blocks deletion, wait for Google to release it.
+
+Then run the same destroy command. Do not manually delete a `serverless-ipv4-*` address.
+
+Cloud SQL can retain producer network resources for up to four days. The package abandons the connection while Google completes that cleanup.
+
+The stack does not own the Pulumi state bucket or image repository. Delete those prerequisites in separate, explicit operations.
+
+## Known limits
+
+- The package supports public ingress only.
+- The package is pre-release.
+- The direct package exposes WorkOS inputs but not generic OIDC inputs.
+- An installation without WorkOS does not provide team browser login.
+- The package does not automate disaster recovery or cross-region failover.
+- The repository does not define a minimum operator IAM role.
+- Production teardown requires an operator-owned deprotection procedure.
+- Complete signed release evidence and full product conformance remain open.
+
+## Source and evidence
+
+- [Google Cloud Pulumi package](https://github.com/plannotator/artifact-server/tree/main/deploy/pulumi/gcp)
+- [Package configuration and qualification status](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/gcp/README.md)
+- [Shared cloud deployment contract](https://github.com/plannotator/artifact-server/blob/main/project/spec/cloud-deployment-contract.md)
+- [Live product evidence](https://github.com/plannotator/artifact-server/blob/main/project/evidence/gcp-deployment-product.json)
+- [Update and rollback evidence](https://github.com/plannotator/artifact-server/blob/main/project/evidence/gcp-upgrade-rollback.json)
+- [State recovery evidence](https://github.com/plannotator/artifact-server/blob/main/project/evidence/gcp-state-recovery.json)
+- [Backup and restore evidence](https://github.com/plannotator/artifact-server/blob/main/project/evidence/gcp-phase11-qualification.json)
+- [Safe deletion evidence](https://github.com/plannotator/artifact-server/blob/main/project/evidence/gcp-destroy.json)
diff --git a/apps/site/src/content/docs/docs/deploy/index.mdx b/apps/site/src/content/docs/docs/deploy/index.mdx
index 078d8d8..9f884d3 100644
--- a/apps/site/src/content/docs/docs/deploy/index.mdx
+++ b/apps/site/src/content/docs/docs/deploy/index.mdx
@@ -1,64 +1,64 @@
---
title: Deploy for a team
-description: Compare the tested deployment targets and the boundaries that every remote installation uses.
+description: Compare the deployment targets, data layers, and verified qualification boundaries.
---
-Artifact Server runs locally and on four server shapes. The qualification level differs by target.
+Artifact Server provides five team deployment guides. The Compose guide covers compact and external-storage modes.
## Choose a deployment
-| Deployment | Data layer | Qualification | Guide |
+| Deployment | Data layer | Verified qualification | Guide |
| --- | --- | --- | --- |
| Cloudflare | D1 and R2 | Live staging passed | [Deploy on Cloudflare](/docs/deploy/cloudflare/) |
-| Compact Compose | SQLite and one file volume | Automated runtime passed | [Compose guide](https://github.com/plannotator/artifact-server/blob/main/packaging/compose/README.md) |
-| External-storage Compose | PostgreSQL and S3-compatible storage | Automated runtime passed | [Compose guide](https://github.com/plannotator/artifact-server/blob/main/packaging/compose/README.md) |
-| Kubernetes | PostgreSQL and object storage | Helm and kind verification passed | [Helm guide](https://github.com/plannotator/artifact-server/blob/main/packaging/helm/artifact-server/README.md) |
-| AWS (Amazon Web Services) | ECS, RDS, and S3 | Pulumi verification passed. No live application deployment. | [AWS Pulumi guide](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/aws/README.md) |
-| Google Cloud (GCP) | Cloud Run, Cloud SQL, and Cloud Storage | Pulumi verification passed. No live application deployment. | [Google Cloud Pulumi guide](https://github.com/plannotator/artifact-server/blob/main/deploy/pulumi/gcp/README.md) |
+| Compact Compose | SQLite and one file volume | Automated runtime passed | [Deploy with Compose](/docs/deploy/compose/) |
+| External-storage Compose | PostgreSQL and S3-compatible storage | Automated runtime passed | [Deploy with Compose](/docs/deploy/compose/) |
+| Kubernetes | PostgreSQL and object storage | Helm and disposable kind-cluster verification passed | [Deploy on Kubernetes](/docs/deploy/kubernetes/) |
+| AWS (Amazon Web Services) | ECS Fargate, RDS, and S3 | Default public stack passed live isolated qualification | [Deploy on AWS](/docs/deploy/aws/) |
+| Google Cloud (GCP) | Cloud Run, Cloud SQL, and Cloud Storage | Public stack passed live isolated qualification | [Deploy on Google Cloud](/docs/deploy/google-cloud/) |
-Cloudflare is the live-qualified hosted target. [Cloudflare Artifacts](/docs/deploy/cloudflare/) can add private Git history for selected projects.
+The AWS private-ingress variant has not passed its live lifecycle gate. The Google Cloud package rejects private ingress.
-AWS and Google Cloud include verified Pulumi projects. Their shared PostgreSQL and object-storage runtime passes automated verification. Neither installer completed a live application deployment.
-
-Azure teams use the Helm chart on AKS. Artifact Server has no separate Azure installer. The Azure Blob Storage adapter remains a preview.
+Use the Kubernetes guide for EKS, GKE, or AKS. Artifact Server does not provide a separate Azure installer.
-## The boundaries every remote deployment shares
+## Meet the remote deployment requirements
-
- A single HTTPS origin serves Artifact Server, its API, and its MCP endpoint. This is where people sign in and where agents connect.
+
+ Use one HTTPS origin for the application, API, and MCP endpoint. People sign in at this origin.
-
- Untrusted artifact files are served from a separate wildcard domain, one hostname per version. The application origin never serves artifact bytes. [Why the two origins are separate →](/docs/concepts/security-boundary/)
+
+ Use a separate registrable domain with wildcard TLS. Each artifact version receives its own hostname.
+
+ [Read about the two-origin security boundary →](/docs/concepts/security-boundary/)
-
- Remote deployments authenticate through WorkOS or one generic OIDC provider. Local-owner access works only on an exact loopback origin. Remote deployments never use local-owner access. Network access and application authorization remain separate controls. An administrator admits team members inside Artifact Server.
+
+ Use WorkOS or one generic OIDC provider. Remote deployments do not use local-owner access.
-
- Service credentials live in the platform's secret store, never in the repository or the image.
+
+ Store service credentials in the deployment platform's secret store. Keep credentials out of source control and deployment outputs.
-
- Back up metadata and artifact files as one recovery set. Use the procedure in the selected guide. Do a restore test before the first production release. Do another restore test after a storage or deployment change.
+
+ Back up metadata and artifact files as one recovery set. Verify a restore before production use.
-
- Pin the production deployment to an immutable image digest. Verify the release as described in [supply-chain evidence](/docs/security/).
+
+ Pin production containers to an immutable image digest. Verify the release with the [supply-chain guide](/docs/security/).
-## Pick a data layer
+## Choose a data layer
-**Compact Compose** uses one SQLite database and one file volume. Run only one application process with this data layer.
+**Compact Compose** uses one SQLite database and one file volume. Run one application process with this mode.
-**External storage** uses PostgreSQL and object storage. Kubernetes, AWS, Google Cloud, and external-storage Compose use this data layer. Application processes can scale horizontally.
+**External storage** uses PostgreSQL and object storage. Compose, Kubernetes, AWS, and Google Cloud support this data layer.
-**Cloudflare** uses D1 and R2 through the Worker runtime, with the same product services as the other targets.
+**Cloudflare** uses D1 and R2 through the Worker runtime. It uses the same product services as the other targets.
-## After deploying
+## Continue after deployment
-- [Welcome your team](/docs/connect-agents/): the introduction for each teammate.
-- [Connect agents to the MCP server](/docs/mcp/): one command for each client.
-- [Review with coding agents](/docs/agents/): live feedback for Pi, OpenCode, and Claude Code.
+- [Welcome your team](/docs/connect-agents/).
+- [Connect agents to the MCP server](/docs/mcp/).
+- [Review with coding agents](/docs/agents/).
diff --git a/apps/site/src/content/docs/docs/deploy/kubernetes.mdx b/apps/site/src/content/docs/docs/deploy/kubernetes.mdx
new file mode 100644
index 0000000..1f4af80
--- /dev/null
+++ b/apps/site/src/content/docs/docs/deploy/kubernetes.mdx
@@ -0,0 +1,338 @@
+---
+title: Deploy on Kubernetes
+description: Install Artifact Server in an existing Kubernetes cluster with Helm, PostgreSQL, and S3-compatible object storage.
+sidebar:
+ order: 3
+---
+
+Use this guide to run Artifact Server in an existing Kubernetes cluster. The Helm chart installs only the application workload.
+
+You must supply PostgreSQL, S3-compatible object storage, DNS, TLS certificates, routing, secrets, and one browser identity provider.
+
+
+
+The chart source is in [`packaging/helm/artifact-server`](https://github.com/plannotator/artifact-server/tree/main/packaging/helm/artifact-server). A public chart package is not available yet.
+
+## Requirements
+
+| Requirement | Supported configuration |
+| --- | --- |
+| Kubernetes | Versions 1.34, 1.35, or 1.36 |
+| Helm | Version 4.2.x |
+| Application image | The released OCI image, pinned by SHA-256 digest |
+| Database | An existing PostgreSQL database |
+| Object storage | An existing S3-compatible bucket |
+| Routing | An Ingress, Gateway, or provider edge that routes two hosts |
+| TLS | One application certificate and one wildcard-content certificate |
+| Browser authentication | WorkOS or one OpenID Connect (OIDC) provider |
+| Secrets | One existing Kubernetes Secret in the release namespace |
+| Recovery | Provider procedures for PostgreSQL and object storage |
+
+Run the commands in this guide from the repository root. Use a released image digest for a team installation.
+
+## Prepare the external services
+
+Create one PostgreSQL database for the installation. Create one S3-compatible bucket for the same installation.
+
+Use encrypted provider connections. Give Artifact Server access only to its database and bucket.
+
+When possible, use a dedicated bucket. A dedicated bucket makes complete backup and restore operations easier to define.
+
+Create these two public DNS names:
+
+- `artifacts.example.com` for the trusted application.
+- `*.content.example.net` for isolated artifact versions.
+
+The two names must use separate registrable domains. The routing layer sends both names to the same private `ClusterIP` Service.
+
+## Create the runtime Secret
+
+Keep all credential values outside the repository and Helm values. The chart mounts Secret keys as read-only files.
+
+Create the bootstrap service key in `as_key_key__` form:
+
+```sh
+install -d -m 0700 /secure/artifact-server
+printf 'as_key_key_%s_%s\n' "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" \
+ > /secure/artifact-server/api-token
+chmod 0400 /secure/artifact-server/api-token
+```
+
+Place the PostgreSQL connection URL in `/secure/artifact-server/database-url`. Protect this file with mode `0400`.
+
+If you use static S3 credentials, place them in two files. When you use workload identity, skip these files.
+
+```text
+/secure/artifact-server/s3-access-key-id
+/secure/artifact-server/s3-secret-access-key
+```
+
+Create the namespace and Secret for static S3 credentials:
+
+```sh
+kubectl create namespace artifact-server
+
+kubectl --namespace artifact-server create secret generic artifact-server-runtime \
+ --from-file=api-token=/secure/artifact-server/api-token \
+ --from-file=database-url=/secure/artifact-server/database-url \
+ --from-file=s3-access-key-id=/secure/artifact-server/s3-access-key-id \
+ --from-file=s3-secret-access-key=/secure/artifact-server/s3-secret-access-key
+```
+
+If you use workload identity, create the Secret without the two S3 key files:
+
+```sh
+kubectl --namespace artifact-server create secret generic artifact-server-runtime \
+ --from-file=api-token=/secure/artifact-server/api-token \
+ --from-file=database-url=/secure/artifact-server/database-url
+```
+
+The first startup stores the bootstrap service key as a managed API key. Later startups require the exact same key.
+
+Protect the Secret with your cluster recovery system. A restored database will reject a different bootstrap key.
+
+## Configure browser authentication
+
+Each team installation requires exactly one browser identity provider. The chart rejects an incomplete provider configuration.
+
+### Generic OIDC
+
+Register this redirect URI with the provider:
+
+```text
+https://artifacts.example.com/auth/callback
+```
+
+Use the exact issuer URL and client ID in the Helm values. The default scope string is `openid email profile`.
+
+If the client requires a secret, add the secret file to `artifact-server-runtime`. Then name its key with `secret.keys.oidcClientSecret`.
+
+### WorkOS
+
+Set `identity.workosClientId` and `identity.workosIssuer` in the Helm values. The issuer is the exact HTTPS AuthKit origin.
+
+Add the WorkOS API key to `artifact-server-runtime`. Then name its key with `secret.keys.workosApiKey`.
+
+Do not configure WorkOS and OIDC together.
+
+## Create the Helm values
+
+Create `artifact-server-values.yaml`. Replace every example value before installation.
+
+```yaml
+replicaCount: 2
+
+image:
+ repository: ghcr.io/plannotator/artifact-server
+ digest: sha256:REPLACE_WITH_THE_RELEASE_DIGEST
+
+configuration:
+ installationId: team-example
+ bootstrapAdministratorEmail: admin@example.com
+ applicationOrigin: https://artifacts.example.com
+ contentDomain: content.example.net
+ postgresConnectionBudget: 22
+ postgresPoolSize: 10
+ s3:
+ bucket: artifact-server-team-example
+ region: us-east-1
+
+identity:
+ oidcClientId: artifact-server
+ oidcIssuer: https://idp.example.com/realms/main
+
+secret:
+ name: artifact-server-runtime
+ rolloutChecksum: secret-version-1
+ keys:
+ s3AccessKeyId: s3-access-key-id
+ s3SecretAccessKey: s3-secret-access-key
+```
+
+The chart rejects a mutable image tag by default. `image.allowMutableTag` exists only for local chart development.
+
+The example connection budget covers two server pools, one migration connection, and one cleanup connection.
+
+Use this formula for another replica count:
+
+```text
+(replicaCount × postgresPoolSize) + migration connection + cleanup connection ≤ postgresConnectionBudget
+```
+
+The chart rejects values that exceed the connection budget.
+
+## Configure workload identity
+
+When the workload uses its cloud identity, leave both S3 key names empty.
+
+Add the provider annotations to the service account:
+
+```yaml
+secret:
+ keys:
+ s3AccessKeyId: ""
+ s3SecretAccessKey: ""
+
+serviceAccount:
+ annotations:
+ example.com/workload-identity: replace-with-provider-value
+ automountServiceAccountToken: false
+```
+
+Replace the example annotation with the annotation for your identity system.
+
+If the identity system requires the projected Kubernetes token, set `automountServiceAccountToken` to `true`.
+
+Otherwise, keep `automountServiceAccountToken` set to `false`.
+
+## Configure routing and TLS
+
+The chart creates a private `ClusterIP` Service. Ingress is disabled by default.
+
+If your controller uses `networking.k8s.io/v1`, enable the included Ingress:
+
+```yaml
+ingress:
+ enabled: true
+ className: nginx
+ applicationTlsSecretName: artifact-server-application-tls
+ contentTlsSecretName: artifact-server-content-tls
+```
+
+The application TLS Secret must cover `artifacts.example.com`. The content TLS Secret must cover `*.content.example.net`.
+
+The included Ingress requires an HTTPS application origin without a path or explicit port.
+
+If you use Gateway API or a provider edge, leave `ingress.enabled` set to `false`. Route both hosts to the chart Service.
+
+## Install Artifact Server
+
+Install the chart with Helm:
+
+```sh
+helm upgrade --install artifact-server ./packaging/helm/artifact-server \
+ --namespace artifact-server \
+ --values artifact-server-values.yaml \
+ --rollback-on-failure \
+ --wait
+```
+
+Helm runs `artifactserver migrate apply` before each installation and upgrade. A failed migration blocks the Deployment change.
+
+Successful migration Jobs are removed. Failed migration Jobs remain for inspection until the next Helm attempt.
+
+Inspect a failed migration:
+
+```sh
+kubectl --namespace artifact-server logs job/artifact-server-migrate
+```
+
+Correct the provider or Secret failure. Then run the installation command again.
+
+## Verify the installation
+
+Verify the rollout and the internal readiness endpoint:
+
+```sh
+kubectl --namespace artifact-server rollout status deployment/artifact-server
+helm test artifact-server --namespace artifact-server --logs
+kubectl --namespace artifact-server get pods,service,cronjob
+```
+
+The chart uses `/health` for startup and liveness probes. It uses `/ready` for readiness and the Helm test.
+
+Verify both public and account-required delivery through the final HTTPS hosts. Then verify browser login and the `/mcp` endpoint.
+
+Use the [publishing guide](/docs/publish/) and the [MCP guide](/docs/mcp/) to verify this product behavior.
+
+## Staging cleanup
+
+The chart creates a cleanup `CronJob` by default. It runs one bounded cleanup pass every 15 minutes.
+
+Serving Pods use the `external` cleanup schedule. They do not run a second cleanup loop.
+
+Change the schedule or limits under `cleanup` in the Helm values.
+
+If another operator runs this command, you can disable the `CronJob`:
+
+```sh
+artifactserver maintenance cleanup-staging --once --mode external-storage
+```
+
+## Back up and restore
+
+The chart does not create or restore database and object backups. Use the recovery procedures for your PostgreSQL and object providers.
+
+Treat the database, the complete installation object set, and the runtime Secret as one recovery set.
+
+Before each upgrade, complete these actions:
+
+1. Stop write traffic at the trusted application edge.
+2. Suspend the cleanup `CronJob`.
+3. Scale the Artifact Server Deployment to zero replicas.
+4. Create a transaction-consistent PostgreSQL backup.
+5. Copy the complete installation object set.
+6. Record the installation ID, image digest, provider backup IDs, and backup time.
+7. Scale the Deployment to its configured replica count.
+8. Resume the cleanup `CronJob`.
+9. Restore traffic at the trusted application edge.
+
+Restore both providers into empty targets. Use the same installation ID and the exact runtime Secret from the recovery set.
+
+After restoration, install the same image digest. Then verify readiness, stable IDs, private access, and exact artifact bytes.
+
+## Upgrade and rollback
+
+Create and verify one recovery set before each upgrade. Then replace `image.digest` with the new release digest.
+
+Run the installation command again. The migration Job completes before the rolling Deployment change.
+
+The default Deployment uses two replicas, zero unavailable Pods, and one surge Pod during a rolling update.
+
+Verify the new release before you remove the recovery set.
+
+Helm rollback does not reverse a database migration. Verify that the old image supports the current schema before rollback.
+
+List the release revisions:
+
+```sh
+helm history artifact-server --namespace artifact-server
+```
+
+Roll back to one compatible revision:
+
+```sh
+helm rollback artifact-server --namespace artifact-server --wait
+helm test artifact-server --namespace artifact-server --logs
+```
+
+## Uninstall
+
+Remove the Artifact Server workloads:
+
+```sh
+helm uninstall artifact-server --namespace artifact-server --wait
+```
+
+This command does not remove the external Secret, PostgreSQL database, bucket, DNS records, or TLS certificates.
+
+Verify a complete recovery set before you remove any durable provider resource.
+
+## Current limits
+
+- The chart does not create a cluster, database, bucket, DNS zone, certificate, Ingress controller, or secret manager.
+- The chart does not include a Horizontal Pod Autoscaler.
+- The chart leaves `NetworkPolicy` disabled until the operator supplies exact rules for the cluster network.
+- The kind gate uses `ClusterIP` and port forwarding. It does not verify an Ingress controller or wildcard TLS.
+- The kind gate uses placeholder OIDC configuration. It does not complete browser authentication.
+- The kind gate does not verify the scheduled execution of the cleanup `CronJob`.
+- The kind gate preserves external providers through uninstall and reinstall. It does not perform a coordinated provider restore.
+- The bounded kind read sample is a regression baseline. It is not a production capacity claim.
+
+Read the [chart README](https://github.com/plannotator/artifact-server/blob/main/packaging/helm/artifact-server/README.md), [`values.yaml`](https://github.com/plannotator/artifact-server/blob/main/packaging/helm/artifact-server/values.yaml), and the [runtime qualification test](https://github.com/plannotator/artifact-server/blob/main/tests/release/helm-chart.test.ts) for implementation details.
diff --git a/apps/site/src/content/docs/docs/index.mdx b/apps/site/src/content/docs/docs/index.mdx
index 6736253..37465ab 100644
--- a/apps/site/src/content/docs/docs/index.mdx
+++ b/apps/site/src/content/docs/docs/index.mdx
@@ -80,3 +80,7 @@ The [@plannotator/agent-bridge](https://github.com/plannotator/agent-bridge) pro
- [Verify software supply-chain evidence](/docs/security/)
- [Deploy for a team](/docs/deploy/)
- [Deploy the Cloudflare package](/docs/deploy/cloudflare/)
+- [Deploy with Compose](/docs/deploy/compose/)
+- [Deploy on Kubernetes](/docs/deploy/kubernetes/)
+- [Deploy on AWS](/docs/deploy/aws/)
+- [Deploy on Google Cloud](/docs/deploy/google-cloud/)
diff --git a/apps/site/src/pages/index.md.ts b/apps/site/src/pages/index.md.ts
index 951bdf0..47bcf23 100644
--- a/apps/site/src/pages/index.md.ts
+++ b/apps/site/src/pages/index.md.ts
@@ -34,6 +34,10 @@ Open source under the GNU Affero General Public License v3.0 (AGPL-3.0-only): us
- [Review with coding agents](https://artifactserver.com/docs/agents/index.md)
- [Deploy for a team](https://artifactserver.com/docs/deploy/index.md)
- [Cloudflare deployment](https://artifactserver.com/docs/deploy/cloudflare/index.md)
+- [Compose deployment](https://artifactserver.com/docs/deploy/compose/index.md)
+- [Kubernetes deployment](https://artifactserver.com/docs/deploy/kubernetes/index.md)
+- [AWS deployment](https://artifactserver.com/docs/deploy/aws/index.md)
+- [Google Cloud deployment](https://artifactserver.com/docs/deploy/google-cloud/index.md)
- [Source](https://github.com/plannotator/artifact-server)
`;
diff --git a/packaging/compose/README.md b/packaging/compose/README.md
index 9c701c3..21d7d64 100644
--- a/packaging/compose/README.md
+++ b/packaging/compose/README.md
@@ -193,11 +193,10 @@ recommended by this production package.
### Configure providers and secrets
-Copy the external-storage environment example and create the two required
-secret files:
+Copy the shared environment example. Then create the two required secret files:
```sh
-cp .env.external-storage.example .env
+cp .env.example .env
install -d -o 1000 -g 1000 -m 0700 /etc/artifact-server
printf 'as_key_key_%s_%s\n' "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" \
> /etc/artifact-server/api-token
diff --git a/project/spec/cloud-deployment-contract.md b/project/spec/cloud-deployment-contract.md
index acca9fe..0f8e2fe 100644
--- a/project/spec/cloud-deployment-contract.md
+++ b/project/spec/cloud-deployment-contract.md
@@ -1,8 +1,8 @@
# Cloud deployment contract
-Status: shared executable contract and AWS and GCP provider packages
-implemented; every target remains gated by its own unfinished real-cloud release
-qualification
+Status: shared executable contract and AWS and GCP provider packages implemented.
+The public AWS and GCP variants passed isolated live qualification. Signed
+release evidence and the AWS private-ingress qualification remain open.
This document is the handoff boundary for the Cloudflare, AWS, and GCP
deployment work. It says exactly what every package receives, what it creates,