vibecarbon 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/carbon/backup/compose-backup.sh +45 -121
  2. package/carbon/db/Dockerfile +14 -3
  3. package/carbon/docker-compose.dns01.prod.yml +42 -0
  4. package/carbon/docker-compose.metabase.prod.yml +1 -2
  5. package/carbon/docker-compose.n8n.prod.yml +1 -2
  6. package/carbon/docker-compose.prod.yml +9 -7
  7. package/carbon/k8s/base/backup/configmap-walg.yaml +47 -0
  8. package/carbon/k8s/base/backup/cronjob.yaml +67 -92
  9. package/carbon/k8s/base/backup/kustomization.yaml +1 -0
  10. package/carbon/k8s/base/backup/network-policy.yaml +9 -14
  11. package/carbon/k8s/base/backup/rbac.yaml +40 -0
  12. package/carbon/k8s/base/network-policies.yaml +33 -0
  13. package/carbon/k8s/base/traefik/certificate.yaml +13 -7
  14. package/carbon/k8s/values/supabase.values.yaml +152 -0
  15. package/carbon/scripts/docker-up.js +13 -1
  16. package/carbon/src/client/components/DeploymentScenariosSection.tsx +171 -0
  17. package/carbon/src/client/locales/de.json +29 -0
  18. package/carbon/src/client/locales/en.json +29 -0
  19. package/carbon/src/client/locales/es.json +29 -0
  20. package/carbon/src/client/locales/fr.json +29 -0
  21. package/carbon/src/client/locales/pt.json +29 -0
  22. package/carbon/src/client/pages/Home.tsx +5 -0
  23. package/package.json +1 -2
  24. package/src/backup.js +13 -66
  25. package/src/create.js +9 -4
  26. package/src/deploy.js +12 -0
  27. package/src/destroy.js +5 -43
  28. package/src/lib/backup-s3.js +0 -31
  29. package/src/lib/cloudflare.js +0 -59
  30. package/src/lib/config.js +1 -42
  31. package/src/lib/deploy/acme.js +57 -0
  32. package/src/lib/deploy/admin-user.js +105 -0
  33. package/src/lib/deploy/bundle.js +137 -5
  34. package/src/lib/deploy/compose/ha.js +208 -92
  35. package/src/lib/deploy/compose/index.js +292 -424
  36. package/src/lib/deploy/k8s/ha/index.js +12 -148
  37. package/src/lib/deploy/k8s/index.js +1 -21
  38. package/src/lib/deploy/k8s/k3s.js +407 -168
  39. package/src/lib/deploy/orchestrator.js +90 -33
  40. package/src/lib/deploy/utils.js +20 -0
  41. package/src/lib/hetzner-guided-setup.js +0 -7
  42. package/src/lib/iac/index.js +45 -16
  43. package/src/lib/images.js +17 -0
  44. package/src/lib/licensing/index.js +1 -59
  45. package/src/lib/licensing/tiers.js +0 -8
  46. package/src/lib/pod-backups.js +53 -0
  47. package/src/lib/providers/index.js +0 -47
  48. package/src/lib/ssh.js +12 -0
  49. package/src/restore.js +216 -302
  50. package/src/scale.js +76 -77
  51. package/carbon/backup/Dockerfile +0 -75
  52. package/carbon/backup/backup.sh +0 -95
  53. package/carbon/runtime.Dockerfile +0 -17
  54. package/src/lib/deploy/compose/acme-verify.js +0 -121
  55. package/src/lib/deploy/index.js +0 -119
  56. package/src/lib/kubectl.js +0 -81
@@ -93,3 +93,36 @@ spec:
93
93
  port: 53
94
94
  - protocol: TCP
95
95
  port: 53
96
+
97
+ ---
98
+ # Allow the supabase-db pod to egress to S3 (HTTPS) for wal-g: continuous WAL
99
+ # archiving (archive_command → wal-g wal-push) and the nightly base backup
100
+ # (kubectl exec → wal-g backup-push) both run INSIDE the db container and push
101
+ # to Hetzner Object Storage. Without this, the implicit default-deny-egress
102
+ # (allow-dns/allow-intra-namespace-egress select all pods with Egress type)
103
+ # drops the db pod's outbound 443 and wal-g hangs/fails.
104
+ # Selector matches the chart-generated db pod labels; base kustomize `labels:`
105
+ # does not include selectors, so these are not mutated.
106
+ apiVersion: networking.k8s.io/v1
107
+ kind: NetworkPolicy
108
+ metadata:
109
+ name: supabase-db-s3-egress
110
+ namespace: vibecarbon
111
+ spec:
112
+ podSelector:
113
+ matchLabels:
114
+ app.kubernetes.io/name: supabase-db
115
+ app.kubernetes.io/instance: supabase
116
+ policyTypes:
117
+ - Egress
118
+ egress:
119
+ - to:
120
+ - ipBlock:
121
+ cidr: 0.0.0.0/0
122
+ except:
123
+ - 10.0.0.0/8
124
+ - 172.16.0.0/12
125
+ - 192.168.0.0/16
126
+ ports:
127
+ - protocol: TCP
128
+ port: 443
@@ -1,11 +1,17 @@
1
1
  # TLS Certificate for Vibecarbon application
2
2
  # Requires cert-manager to be installed (kubectl apply -k k8s/infra/)
3
3
  #
4
- # issuerRef.name is patched per-deployment by applyK3sManifests in
5
- # src/lib/deploy/k8s/k3s.js — it picks letsencrypt-{prod,staging}-{
6
- # cloudflare,hetzner,manual} based on dnsProvider + ACME_CA_SERVER.
7
- # The placeholder below keeps this template valid for kustomize and
8
- # is overwritten before cert-manager ever sees a Certificate request.
4
+ # issuerRef.name and dnsNames are BOTH patched per-deployment by
5
+ # applyK3sManifests in src/lib/deploy/k8s/k3s.js:
6
+ # - issuerRef.name → letsencrypt-{prod,staging}-{cloudflare,hetzner,manual}
7
+ # based on dnsProvider + ACME_CA_SERVER.
8
+ # - dnsNames [domain] for manual (HTTP-01) issuers;
9
+ # [domain, "*.domain"] for DNS-01 (cloudflare/hetzner) issuers.
10
+ # DNS-01 allows a wildcard SAN so one cert covers every subdomain
11
+ # (api, studio, dashboard, grafana, n8n, ...) without separate ACME orders.
12
+ #
13
+ # The placeholders below keep this template valid for kustomize; they are
14
+ # overwritten before cert-manager ever sees a Certificate request.
9
15
  apiVersion: cert-manager.io/v1
10
16
  kind: Certificate
11
17
  metadata:
@@ -17,5 +23,5 @@ spec:
17
23
  kind: ClusterIssuer
18
24
  dnsNames:
19
25
  - app.example.com # Patched with actual domain at deploy time
20
- # Optional: Include additional subdomains
21
- # - "*.app.example.com"
26
+ # For DNS-01 (managed-DNS) deploys the wildcard is added at deploy time:
27
+ # - "*.app.example.com"
@@ -54,6 +54,18 @@ secret:
54
54
  password: DB_PASSWORD
55
55
  database: DB_NAME
56
56
 
57
+ # Override the db image with vibecarbon's wal-g-equipped build (carbon/db/
58
+ # Dockerfile: FROM supabase/postgres + wal-g v3.0.5). The stock chart image
59
+ # has no wal-g, which is why k8s historically had no WAL archiving / PITR.
60
+ # Built + sideloaded to the supabase node by deployK3s (k3s-db-build /
61
+ # k3s-db-sideload); IfNotPresent so the sideloaded image is used, not pulled.
62
+ # {{DB_IMAGE}}/{{DB_IMAGE_TAG}} substituted at deploy time in installSupabase.
63
+ image:
64
+ db:
65
+ repository: "{{DB_IMAGE}}"
66
+ tag: "{{DB_IMAGE_TAG}}"
67
+ pullPolicy: IfNotPresent
68
+
57
69
  # Component enable toggles — chart's path is deployment.<comp>.enabled.
58
70
  # Top-level `functions:`/`analytics:`/`vector:` are silently ignored
59
71
  # (was the bug behind the 2026-04-26 e2e helm-wait timeout).
@@ -81,6 +93,129 @@ deployment:
81
93
  operator: Equal
82
94
  value: supabase
83
95
  effect: NoSchedule
96
+ # Headroom for the daily wal-g base backup, which runs INSIDE this
97
+ # container's cgroup (via `kubectl exec`, see k8s/base/backup/cronjob.yaml).
98
+ # backup-push streams PGDATA → lz4 → S3; size the limit for the backup
99
+ # window, not steady state, or it throttles postgres too. WALG_UPLOAD_
100
+ # CONCURRENCY is kept at 2 (vs compose's 4) to bound the burst.
101
+ resources:
102
+ requests:
103
+ cpu: 250m
104
+ memory: 512Mi
105
+ limits:
106
+ cpu: "2"
107
+ memory: 2Gi
108
+ # Mount the fault-tolerant WAL-archive wrapper that postgres'
109
+ # archive_command calls (enabled at runtime via ALTER SYSTEM in
110
+ # applyK3sManifests). Script body lives in the vibecarbon-wal-archive
111
+ # ConfigMap (k8s/base/backup/configmap-walg.yaml) — verbatim copy of
112
+ # carbon/volumes/db/wal-archive.sh (retry + exit-0 prevents disk-fill).
113
+ volumeMounts:
114
+ - name: wal-archive-script
115
+ mountPath: /etc/postgresql/wal-archive.sh
116
+ subPath: wal-archive.sh
117
+ readOnly: true
118
+ - name: walg-s3-creds
119
+ mountPath: /etc/walg
120
+ readOnly: true
121
+ volumes:
122
+ - name: wal-archive-script
123
+ configMap:
124
+ # Invoked as `bash /etc/postgresql/wal-archive.sh` (archive_command),
125
+ # so no exec bit is required on the mounted file.
126
+ name: vibecarbon-wal-archive
127
+ - name: walg-s3-creds
128
+ secret:
129
+ secretName: vibecarbon-secrets
130
+ # Optional so a no-S3 (dev) deploy still schedules the db pod; wal-g
131
+ # is simply unconfigured then. items maps the INI key → credentials.
132
+ optional: true
133
+ items:
134
+ - key: S3_CREDENTIALS_INI
135
+ path: credentials
136
+ # Restore bootstrap: a marker-gated init container that, ONLY when a
137
+ # restore is requested (RESTORE_TARGET set in vibecarbon-secrets), fetches
138
+ # the latest wal-g base backup into PGDATA before postgres starts. On a
139
+ # normal boot RESTORE_TARGET is empty and the init container is a no-op.
140
+ # Uses the same wal-g-equipped db image. RESTORE_TARGET is an OPTIONAL
141
+ # secretKeyRef so deploys without the key still schedule.
142
+ extraInitContainers:
143
+ - name: walg-restore
144
+ image: "{{DB_IMAGE}}:{{DB_IMAGE_TAG}}"
145
+ imagePullPolicy: IfNotPresent
146
+ command:
147
+ - /bin/bash
148
+ - -c
149
+ - |
150
+ set -euo pipefail
151
+ if [ -z "${RESTORE_TARGET:-}" ]; then
152
+ echo "walg-restore: RESTORE_TARGET empty — normal boot, skipping."
153
+ exit 0
154
+ fi
155
+ echo "walg-restore: RESTORE_TARGET=${RESTORE_TARGET} — restoring from S3."
156
+ if [ -f "${PGDATA}/PG_VERSION" ]; then
157
+ echo "walg-restore: existing PGDATA — clearing for backup-fetch."
158
+ rm -rf "${PGDATA:?}/"* "${PGDATA:?}/".* 2>/dev/null || true
159
+ fi
160
+ # Fetch the latest base backup into PGDATA. (For PITR the base that
161
+ # precedes the target is also the latest base — base backups are
162
+ # frequent, so LATEST is the right starting point either way.)
163
+ wal-g backup-fetch "${PGDATA}" LATEST
164
+ # Configure archive recovery. A fetched base backup is only
165
+ # consistent AFTER replaying the WAL from its redo point to its
166
+ # stop point — and that WAL lives in S3, not in the fetched PGDATA.
167
+ # Without restore_command + recovery.signal postgres aborts startup
168
+ # with "could not locate required checkpoint record" (the bug this
169
+ # fixes — the `latest` branch previously did neither). restore_command
170
+ # pulls each segment back from S3 via `wal-g wal-fetch`; recovery.signal
171
+ # puts postgres into archive recovery; on reaching the end of WAL (or
172
+ # the PITR target) it promotes to read-write. (RCA 2026-05-31: e2e
173
+ # k8s/k8s-ha restore — postgres crash-looped post-fetch.)
174
+ {
175
+ echo "restore_command = 'wal-g wal-fetch \"%f\" \"%p\"'"
176
+ echo "recovery_target_action = 'promote'"
177
+ # Pin recovery to the fetched base backup's OWN timeline. Default
178
+ # 'latest' makes postgres chase the newest timeline with a .history
179
+ # file; in HA, repeated restore→promote cycles leave DIVERGENT
180
+ # timelines in the shared wal-g S3 prefix, and 'latest' can pick one
181
+ # that forked before this base backup → crash-loop "requested
182
+ # timeline N is not a child of this server's history". 'current'
183
+ # recovers along the base backup's timeline, then promotes fresh.
184
+ echo "recovery_target_timeline = 'current'"
185
+ if [ "${RESTORE_TARGET}" != "latest" ]; then
186
+ # PITR: RESTORE_TARGET is an ISO-8601 timestamp — replay only up
187
+ # to that point instead of to the end of the WAL stream.
188
+ echo "recovery_target_time = '${RESTORE_TARGET}'"
189
+ fi
190
+ } >> "${PGDATA}/postgresql.auto.conf"
191
+ touch "${PGDATA}/recovery.signal"
192
+ echo "walg-restore: backup-fetch complete; archive recovery configured."
193
+ env:
194
+ - name: PGDATA
195
+ value: /var/lib/postgresql/data
196
+ - name: RESTORE_TARGET
197
+ valueFrom:
198
+ secretKeyRef:
199
+ name: vibecarbon-secrets
200
+ key: RESTORE_TARGET
201
+ optional: true
202
+ - name: WALG_S3_PREFIX
203
+ value: "{{WALG_S3_PREFIX}}"
204
+ - name: WALG_COMPRESSION_METHOD
205
+ value: lz4
206
+ - name: AWS_ENDPOINT
207
+ value: "{{S3_ENDPOINT}}"
208
+ - name: AWS_REGION
209
+ value: "{{S3_REGION}}"
210
+ - name: AWS_SHARED_CREDENTIALS_FILE
211
+ value: /etc/walg/credentials
212
+ volumeMounts:
213
+ - name: postgres-volume
214
+ mountPath: /var/lib/postgresql/data
215
+ subPath: postgres-data
216
+ - name: walg-s3-creds
217
+ mountPath: /etc/walg
218
+ readOnly: true
84
219
 
85
220
  # Per-component environment — chart's path is environment.<comp>.
86
221
  environment:
@@ -99,6 +234,23 @@ environment:
99
234
  GOTRUE_MAILER_AUTOCONFIRM: "true"
100
235
  realtime:
101
236
  DB_NAME: postgres
237
+ # Non-secret WAL-G env for the db container (continuous WAL archiving +
238
+ # the exec'd base-backup CronJob both read these). The chart renders
239
+ # environment.<comp> as plain `value:` strings (no valueFrom), so the
240
+ # SECRET keys (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) are injected
241
+ # separately via a StatefulSet env patch in applyK3sManifests.
242
+ # {{WALG_S3_PREFIX}} resolves to s3://<backup-bucket|storage-bucket>/
243
+ # backups/<project>/walg (fallback handled in installSupabase).
244
+ db:
245
+ WALG_S3_PREFIX: "{{WALG_S3_PREFIX}}"
246
+ WALG_COMPRESSION_METHOD: lz4
247
+ WALG_UPLOAD_CONCURRENCY: "2"
248
+ AWS_ENDPOINT: "{{S3_ENDPOINT}}"
249
+ AWS_REGION: "{{S3_REGION}}"
250
+ # wal-g (AWS SDK) reads S3 creds from this INI file, mounted from
251
+ # vibecarbon-secrets (deployment.db.volumes below). File-mount, not env,
252
+ # so it's helm-owned and survives every upgrade with no db restart.
253
+ AWS_SHARED_CREDENTIALS_FILE: /etc/walg/credentials
102
254
  storage:
103
255
  STORAGE_BACKEND: s3
104
256
  imgproxy:
@@ -124,7 +124,19 @@ const manifest = loadManifest();
124
124
 
125
125
  // Get port configuration with offset applied
126
126
  const portEnv = getPortEnvVars();
127
- const execOpts = { stdio: 'inherit', env: { ...process.env, ...portEnv } };
127
+
128
+ // Pin local builds to the `default` (docker-driver) BuildKit builder.
129
+ // `docker compose up` delegates image builds to `buildx bake`, which uses the
130
+ // globally-selected builder. A long-lived `docker-container` builder (e.g. a
131
+ // `vc-multiarch` builder left active via `buildx create --use` after a
132
+ // multi-arch release build) freezes its /etc/resolv.conf at creation time — so
133
+ // after a laptop changes network/subnet it keeps querying a dead DNS server and
134
+ // builds fail with "i/o timeout" resolving registry-1.docker.io. The default
135
+ // builder builds through the daemon's live DNS on every build, sidestepping
136
+ // that whole class. Local dev/test never needs multi-arch; an explicit
137
+ // BUILDX_BUILDER in the environment still wins, for anyone who wants otherwise.
138
+ const buildEnv = { BUILDX_BUILDER: process.env.BUILDX_BUILDER || 'default' };
139
+ const execOpts = { stdio: 'inherit', env: { ...process.env, ...portEnv, ...buildEnv } };
128
140
 
129
141
  // Check if any optional services (metabase, n8n, observability, etc.) are enabled.
130
142
  // These can have slow health checks (e.g. Metabase JVM takes 2-4 min), so we start
@@ -0,0 +1,171 @@
1
+ import { motion } from 'framer-motion';
2
+ import { ArrowLeftRight, CornerDownRight, RefreshCw, Server } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import vibecarbonIcon from '../assets/vibecarbon-icon.svg';
5
+
6
+ type Shape = 'single' | 'pair' | 'cluster' | 'multiCluster';
7
+ type Accent = 'teal' | 'magenta';
8
+
9
+ interface Scenario {
10
+ id: string;
11
+ shape: Shape;
12
+ accent: Accent;
13
+ }
14
+
15
+ const ACCENTS: Record<Accent, { text: string; pill: string; cluster: string; hover: string }> = {
16
+ teal: {
17
+ text: 'text-primary',
18
+ pill: 'border-primary/30 text-primary',
19
+ cluster: 'border-primary/30',
20
+ hover: 'hover:border-primary/30',
21
+ },
22
+ magenta: {
23
+ text: 'text-secondary-accent',
24
+ pill: 'border-secondary-accent/30 text-secondary-accent',
25
+ cluster: 'border-secondary-accent/30',
26
+ hover: 'hover:border-secondary-accent/30',
27
+ },
28
+ };
29
+
30
+ const SCENARIOS: Scenario[] = [
31
+ { id: 'compose', shape: 'single', accent: 'teal' },
32
+ { id: 'composeHa', shape: 'pair', accent: 'magenta' },
33
+ { id: 'k8s', shape: 'cluster', accent: 'teal' },
34
+ { id: 'k8sHa', shape: 'multiCluster', accent: 'magenta' },
35
+ ];
36
+
37
+ // A single server "node" — same visual language as WorkflowSection's DeployVisual pods.
38
+ function Node({ color }: { color: string }) {
39
+ return (
40
+ <div className="flex items-center justify-center rounded-md border border-border/60 bg-background/60 p-1.5 dark:border-white/10 dark:bg-white/[0.03]">
41
+ <Server className={`size-3.5 ${color}`} strokeWidth={1.5} />
42
+ </div>
43
+ );
44
+ }
45
+
46
+ // A dashed "cluster" outline wrapping N nodes. `count` is a fixed literal per topology
47
+ // (2 or 3), so naming the nodes gives each a stable, reorder-free key.
48
+ const NODE_KEYS = ['n1', 'n2', 'n3'];
49
+
50
+ function Cluster({ color, border, count }: { color: string; border: string; count: number }) {
51
+ return (
52
+ <div className={`flex items-center gap-1 rounded-lg border border-dashed ${border} p-1.5`}>
53
+ {NODE_KEYS.slice(0, count).map((key) => (
54
+ <Node key={key} color={color} />
55
+ ))}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ // Topology glyph: 1 node / 2 nodes + failover / 3-node cluster / 2 clusters + region failover.
61
+ function Topology({ shape, accent }: { shape: Shape; accent: Accent }) {
62
+ const { text, cluster } = ACCENTS[accent];
63
+ switch (shape) {
64
+ case 'single':
65
+ return <Node color={text} />;
66
+ case 'pair':
67
+ return (
68
+ <div className="flex items-center gap-2">
69
+ <Node color={text} />
70
+ <RefreshCw className={`size-3.5 ${text}`} strokeWidth={1.5} />
71
+ <Node color={text} />
72
+ </div>
73
+ );
74
+ case 'cluster':
75
+ return <Cluster color={text} border={cluster} count={3} />;
76
+ case 'multiCluster':
77
+ return (
78
+ <div className="flex items-center gap-2">
79
+ <Cluster color={text} border={cluster} count={2} />
80
+ <ArrowLeftRight className={`size-3.5 ${text}`} strokeWidth={1.5} />
81
+ <Cluster color={text} border={cluster} count={2} />
82
+ </div>
83
+ );
84
+ }
85
+ }
86
+
87
+ export function DeploymentScenariosSection() {
88
+ const { t } = useTranslation();
89
+
90
+ return (
91
+ <section className="relative py-24 md:py-36">
92
+ <div className="mx-auto max-w-5xl px-6">
93
+ <div className="mb-10 text-center">
94
+ <motion.div
95
+ className="mb-6 flex justify-center"
96
+ animate={{ y: [0, -6, 0] }}
97
+ transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
98
+ >
99
+ <img
100
+ src={vibecarbonIcon}
101
+ alt=""
102
+ width={56}
103
+ height={56}
104
+ className="drop-shadow-[0_0_12px_oklch(0.82_0.14_192/0.6)]"
105
+ />
106
+ </motion.div>
107
+ <h2 className="mb-4 text-3xl font-black tracking-tight md:text-5xl">
108
+ {t('landing.deploy.headline')}{' '}
109
+ <span className="bg-gradient-to-r from-primary via-cyan-400 to-secondary-accent bg-clip-text text-transparent">
110
+ {t('landing.deploy.headlineHighlight')}
111
+ </span>
112
+ </h2>
113
+ <p className="mx-auto max-w-2xl text-lg text-muted-foreground">
114
+ {t('landing.deploy.subheading')}
115
+ </p>
116
+ </div>
117
+
118
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
119
+ {SCENARIOS.map((scenario, index) => {
120
+ const accent = ACCENTS[scenario.accent];
121
+ const features = t(`landing.deploy.${scenario.id}.features`, {
122
+ returnObjects: true,
123
+ }) as string[];
124
+
125
+ return (
126
+ <motion.div
127
+ key={scenario.id}
128
+ initial={{ opacity: 0, y: 20 }}
129
+ whileInView={{ opacity: 1, y: 0 }}
130
+ viewport={{ once: true, margin: '-50px' }}
131
+ transition={{ duration: 0.4, delay: index * 0.08 }}
132
+ className={`group flex flex-col rounded-2xl border border-border/60 bg-muted/40 p-6 transition-all duration-300 dark:border-white/10 dark:bg-white/5 ${accent.hover} hover:bg-muted/80 dark:hover:bg-white/10`}
133
+ >
134
+ {/* Topology glyph */}
135
+ <div className="mb-5 flex h-12 items-center">
136
+ <Topology shape={scenario.shape} accent={scenario.accent} />
137
+ </div>
138
+
139
+ {/* Name + tagline */}
140
+ <h3 className="text-lg font-bold text-foreground">
141
+ {t(`landing.deploy.${scenario.id}.name`)}
142
+ </h3>
143
+ <p className="mt-1 text-sm text-muted-foreground">
144
+ {t(`landing.deploy.${scenario.id}.tagline`)}
145
+ </p>
146
+
147
+ {/* Attribute pills */}
148
+ <div className="mt-4 flex flex-wrap gap-2">
149
+ {features.map((feature) => (
150
+ <span
151
+ key={feature}
152
+ className={`rounded-full border px-2.5 py-0.5 text-xs font-medium ${accent.pill}`}
153
+ >
154
+ {feature}
155
+ </span>
156
+ ))}
157
+ </div>
158
+
159
+ {/* Best for */}
160
+ <div className="mt-5 flex items-center gap-1.5 text-sm text-muted-foreground">
161
+ <CornerDownRight className={`size-4 shrink-0 ${accent.text}`} strokeWidth={1.5} />
162
+ <span>{t(`landing.deploy.${scenario.id}.bestFor`)}</span>
163
+ </div>
164
+ </motion.div>
165
+ );
166
+ })}
167
+ </div>
168
+ </div>
169
+ </section>
170
+ );
171
+ }
@@ -394,6 +394,35 @@
394
394
  "description": "Jede Route validiert mit Zod, jede Abfrage nutzt den richtigen Supabase-Client, jede Tabelle hat RLS. KI-Agenten replizieren jedes Mal dieselben korrekten Muster."
395
395
  }
396
396
  },
397
+ "deploy": {
398
+ "headline": "Deploye nach deinen Vorstellungen,",
399
+ "headlineHighlight": "mit einem Befehl",
400
+ "subheading": "Von einem einzelnen VPS bis zu Multi-Region-Kubernetes — wähle dein Maß an Skalierung und Ausfallsicherheit. Wechsle jederzeit in die nächste Stufe.",
401
+ "compose": {
402
+ "name": "Docker Compose",
403
+ "tagline": "Ein VPS, dein kompletter Stack inklusive.",
404
+ "features": ["1 Server", "Einzelne Region"],
405
+ "bestFor": "MVPs & interne Tools"
406
+ },
407
+ "composeHa": {
408
+ "name": "Compose HA",
409
+ "tagline": "Zwei Server, automatisches Failover.",
410
+ "features": ["2 Server", "Streaming-Replikation", "Auto-Failover"],
411
+ "bestFor": "Produktions-SaaS ohne Kubernetes"
412
+ },
413
+ "k8s": {
414
+ "name": "Kubernetes",
415
+ "tagline": "Automatisch skalierender k3s-Cluster.",
416
+ "features": ["Einzelne Region", "Horizontale Autoskalierung", "Rollierende Updates"],
417
+ "bestFor": "Anwendungen mit hohem Traffic"
418
+ },
419
+ "k8sHa": {
420
+ "name": "Kubernetes HA",
421
+ "tagline": "Multi-Region-Cluster, maximale Verfügbarkeit.",
422
+ "features": ["2 Regionen", "Regions-Failover", "Autoskalierung"],
423
+ "bestFor": "Geschäftskritisch & global"
424
+ }
425
+ },
397
426
  "pricing": {
398
427
  "headline1": "Von der",
399
428
  "headlineSketch": "Idee",
@@ -394,6 +394,35 @@
394
394
  "description": "Every route validates with Zod, every query uses the right Supabase client, every table has RLS. AI agents replicate the same correct patterns every time."
395
395
  }
396
396
  },
397
+ "deploy": {
398
+ "headline": "Deploy your way,",
399
+ "headlineHighlight": "in one command",
400
+ "subheading": "From a single VPS to multi-region Kubernetes — choose your level of scale and resilience. Grow into the next tier whenever you're ready.",
401
+ "compose": {
402
+ "name": "Docker Compose",
403
+ "tagline": "One VPS, your full stack included.",
404
+ "features": ["1 server", "Single region"],
405
+ "bestFor": "MVPs & internal tools"
406
+ },
407
+ "composeHa": {
408
+ "name": "Compose HA",
409
+ "tagline": "Two servers, automatic failover.",
410
+ "features": ["2 servers", "Streaming replication", "Auto-failover"],
411
+ "bestFor": "Production SaaS without Kubernetes"
412
+ },
413
+ "k8s": {
414
+ "name": "Kubernetes",
415
+ "tagline": "Auto-scaling k3s cluster.",
416
+ "features": ["Single region", "Horizontal autoscaling", "Rolling updates"],
417
+ "bestFor": "High-traffic apps"
418
+ },
419
+ "k8sHa": {
420
+ "name": "Kubernetes HA",
421
+ "tagline": "Multi-region clusters, maximum uptime.",
422
+ "features": ["2 regions", "Region failover", "Autoscaling"],
423
+ "bestFor": "Mission-critical & global"
424
+ }
425
+ },
397
426
  "pricing": {
398
427
  "headline1": "From",
399
428
  "headlineSketch": "sketch",
@@ -394,6 +394,35 @@
394
394
  "description": "Cada ruta valida con Zod, cada consulta usa el cliente Supabase correcto, cada tabla tiene RLS. Los agentes de IA replican los mismos patrones correctos en todo momento."
395
395
  }
396
396
  },
397
+ "deploy": {
398
+ "headline": "Despliega a tu manera,",
399
+ "headlineHighlight": "en un solo comando",
400
+ "subheading": "De un único VPS a Kubernetes multirregión — elige tu nivel de escalado y resiliencia. Sube al siguiente nivel cuando quieras.",
401
+ "compose": {
402
+ "name": "Docker Compose",
403
+ "tagline": "Un VPS, con todo tu stack incluido.",
404
+ "features": ["1 servidor", "Región única"],
405
+ "bestFor": "MVPs y herramientas internas"
406
+ },
407
+ "composeHa": {
408
+ "name": "Compose HA",
409
+ "tagline": "Dos servidores, conmutación automática.",
410
+ "features": ["2 servidores", "Replicación en streaming", "Conmutación automática"],
411
+ "bestFor": "SaaS en producción sin Kubernetes"
412
+ },
413
+ "k8s": {
414
+ "name": "Kubernetes",
415
+ "tagline": "Clúster k3s con autoescalado.",
416
+ "features": ["Región única", "Autoescalado horizontal", "Actualizaciones progresivas"],
417
+ "bestFor": "Apps de alto tráfico"
418
+ },
419
+ "k8sHa": {
420
+ "name": "Kubernetes HA",
421
+ "tagline": "Clústeres multirregión, máxima disponibilidad.",
422
+ "features": ["2 regiones", "Conmutación de región", "Autoescalado"],
423
+ "bestFor": "Críticos y globales"
424
+ }
425
+ },
397
426
  "pricing": {
398
427
  "headline1": "Del",
399
428
  "headlineSketch": "boceto",
@@ -394,6 +394,35 @@
394
394
  "description": "Chaque route valide avec Zod, chaque requête utilise le bon client Supabase, chaque table a RLS. Les agents IA reproduisent les mêmes schémas corrects à chaque fois."
395
395
  }
396
396
  },
397
+ "deploy": {
398
+ "headline": "Déployez à votre façon,",
399
+ "headlineHighlight": "en une commande",
400
+ "subheading": "D'un seul VPS à Kubernetes multi-région — choisissez votre niveau d'évolutivité et de résilience. Passez au palier supérieur quand vous le souhaitez.",
401
+ "compose": {
402
+ "name": "Docker Compose",
403
+ "tagline": "Un VPS, toute votre stack incluse.",
404
+ "features": ["1 serveur", "Région unique"],
405
+ "bestFor": "MVP et outils internes"
406
+ },
407
+ "composeHa": {
408
+ "name": "Compose HA",
409
+ "tagline": "Deux serveurs, bascule automatique.",
410
+ "features": ["2 serveurs", "Réplication en flux", "Bascule automatique"],
411
+ "bestFor": "SaaS en production sans Kubernetes"
412
+ },
413
+ "k8s": {
414
+ "name": "Kubernetes",
415
+ "tagline": "Cluster k3s à mise à l'échelle automatique.",
416
+ "features": ["Région unique", "Mise à l'échelle horizontale", "Mises à jour progressives"],
417
+ "bestFor": "Applications à fort trafic"
418
+ },
419
+ "k8sHa": {
420
+ "name": "Kubernetes HA",
421
+ "tagline": "Clusters multi-région, disponibilité maximale.",
422
+ "features": ["2 régions", "Bascule de région", "Mise à l'échelle auto"],
423
+ "bestFor": "Critique et mondial"
424
+ }
425
+ },
397
426
  "pricing": {
398
427
  "headline1": "Du",
399
428
  "headlineSketch": "brouillon",
@@ -394,6 +394,35 @@
394
394
  "description": "Cada rota valida com Zod, cada consulta usa o cliente Supabase correto, cada tabela tem RLS. Os agentes de IA replicam sempre os mesmos padrões corretos."
395
395
  }
396
396
  },
397
+ "deploy": {
398
+ "headline": "Implante do seu jeito,",
399
+ "headlineHighlight": "com um único comando",
400
+ "subheading": "De um único VPS a Kubernetes multirregião — escolha seu nível de escala e resiliência. Suba para o próximo nível quando quiser.",
401
+ "compose": {
402
+ "name": "Docker Compose",
403
+ "tagline": "Um VPS, com toda a sua stack incluída.",
404
+ "features": ["1 servidor", "Região única"],
405
+ "bestFor": "MVPs e ferramentas internas"
406
+ },
407
+ "composeHa": {
408
+ "name": "Compose HA",
409
+ "tagline": "Dois servidores, failover automático.",
410
+ "features": ["2 servidores", "Replicação em streaming", "Failover automático"],
411
+ "bestFor": "SaaS em produção sem Kubernetes"
412
+ },
413
+ "k8s": {
414
+ "name": "Kubernetes",
415
+ "tagline": "Cluster k3s com escalonamento automático.",
416
+ "features": ["Região única", "Escalonamento horizontal", "Atualizações graduais"],
417
+ "bestFor": "Apps de alto tráfego"
418
+ },
419
+ "k8sHa": {
420
+ "name": "Kubernetes HA",
421
+ "tagline": "Clusters multirregião, disponibilidade máxima.",
422
+ "features": ["2 regiões", "Failover de região", "Escalonamento automático"],
423
+ "bestFor": "Crítico e global"
424
+ }
425
+ },
397
426
  "pricing": {
398
427
  "headline1": "Do",
399
428
  "headlineSketch": "esboço",
@@ -16,6 +16,7 @@ import { AIIntegrationSection } from '../components/AIIntegrationSection';
16
16
  import { ArchitectureSection } from '../components/ArchitectureSection';
17
17
  import { ComparisonSection } from '../components/ComparisonSection';
18
18
  import { CTAFooter } from '../components/CTAFooter';
19
+ import { DeploymentScenariosSection } from '../components/DeploymentScenariosSection';
19
20
  import { FilmGrainOverlay } from '../components/effects/FilmGrainOverlay';
20
21
  import { VGlowEffect } from '../components/effects/VGlowEffect';
21
22
  import { FAQSection } from '../components/FAQSection';
@@ -168,6 +169,10 @@ export default function VibecarbonHome() {
168
169
  <ArchitectureSection />
169
170
  </ScrollSection>
170
171
 
172
+ <ScrollSection id="deploy-scenarios">
173
+ <DeploymentScenariosSection />
174
+ </ScrollSection>
175
+
171
176
  <ScrollSection id="pricing">
172
177
  <PricingSection />
173
178
  </ScrollSection>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibecarbon",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Create and manage production-ready Vibecarbon applications",
5
5
  "type": "module",
6
6
  "bin": "./src/cli.js",
@@ -89,7 +89,6 @@
89
89
  "autocannon": "^8.0.0",
90
90
  "better-sqlite3": "^12.8.0",
91
91
  "conventional-changelog-conventionalcommits": "^9.3.1",
92
- "msw": "^2.12.14",
93
92
  "semantic-release": "^25.0.3",
94
93
  "tsx": "^4.21.0",
95
94
  "vite": "^8.0.14",