Product separation
Appsterisk shares physical infrastructure with Webhookr and shares no logical ownership. This document is the inventory that makes that claim checkable, and the carve-out procedure that makes it useful.
Kept accurate as the system changes. A stale entry here is worse than a missing one: it turns the divestiture plan into a false one, and nobody discovers that until the moment it matters.
Last verified against Terraform state and the live providers: 2026-08-28.
1. Owned outright — transfers by handing over
Repositories (forgers-tech/*, all private)
| Repository | Role |
|---|---|
appsterisk-svc | Domain, persistence, resolver, checkout, webhook. The only service with code today. |
appsterisk-bff | Application API (GraphQL). Empty. |
appsterisk-web | Public profile and owner dashboard. Empty. |
appsterisk-admin | Admin console — its own repo so a build flag cannot expose it publicly. Empty. |
appsterisk-app | Mobile (Expo). Empty. |
appsterisk-docs | This document lives here. |
appsterisk-infra | Terraform root. |
appsterisk-artifacts | Decision records, ADRs, acceptance journeys. |
appsterisk-gitops | ArgoCD desired state. |
Cloud resources — 73 in Terraform state
| Provider | Resources |
|---|---|
| GCP | Project appsterisk-prd; state bucket gs://appsterisk-tfstate; Identity Platform config; Google sign-in IdP config |
| Cloudflare | Zone appsterisk.me; DNSSEC; zone settings (Full strict, HSTS, TLS 1.2+); Origin CA certificate; 12 DNS records; R2 buckets appsterisk-assets and appsterisk-documents |
| Hetzner | Server appsterisk (cx23, nbg1); two persistent primary IPs; firewall restricting 80/443 to Cloudflare ranges |
| Stripe | Webhook endpoint for api.appsterisk.me/v1/stripe/webhook |
| GitHub | 9 repositories; branch-protection rulesets; Dependabot and vulnerability alerts; per-repo APPSTERISK_GITOPS_PAT; read-only deploy key for ArgoCD |
Terraform state
gs://appsterisk-tfstate, prefix terraform/state, in project appsterisk-prd. Separate bucket
in a separate project from Webhookr's — carve-out hands over one bucket, not a state split.
Modules are vendored into appsterisk-infra/modules/, not referenced across repositories, so
transferring the root does not leave dangling sources. modules/tailscale-key is a deliberate
partial copy: the upstream module owns tailscale_acl, a tailnet-wide singleton whose provider
replaces the whole policy on apply — two roots owning it means the last apply silently overwrites
the other product's access policy.
Domain
appsterisk.me. Registrar is GoDaddy; nameservers delegate to Cloudflare; DNSSEC active with the DS
record published at the registrar.
Cost, Appsterisk alone
| Item | Monthly |
|---|---|
Hetzner cx23 | €6.49 |
| Cloudflare (zone, R2, DNS) | €0 at current volume |
| Redis Cloud (free tier, provisioned, unused) | €0 |
| Neon | — shared, see §2 |
| GCP (state bucket, Identity Platform under free tier) | <€1 |
| Total | ≈ €7 |
Every taggable resource carries product=appsterisk; Kubernetes objects carry the same label.
Neon and Redis Cloud are the two lines that cannot be attributed from a bill.
2. Shared with Webhookr — the actual carve-out work
This is the honest part. Each entry says what breaks and what it costs.
Neon Postgres — the heaviest coupling
Databases appsterisk_prd and appsterisk_qa with their own roles, inside the Webhookr Neon
project (D-8). Separate databases isolate schema, credentials and migration history. They do
not isolate compute, connections or autosuspend — and that compute exhausted its quota on
2026-08-19, producing a seven-day undetected outage.
Carve-out: provision a Neon project, pg_dump/pg_restore, cut over the connection string.
A real data migration with downtime, not a credential change. Revisit before meaningful traffic,
not at divestiture.
Redis Cloud
Own subscription and database inside the shared Redis Cloud account. Currently provisioned and not used — the free tier offers no TLS, and a cache that does not exist yet is not worth a password crossing the public internet in the clear.
Carve-out: new account, re-provision. Nothing to migrate while it holds no data.
forgers-tech/github-workflows
appsterisk-svc calls the shared pull-request.yml, build-deploy.yml, scorecard.yml and
security-scheduled.yml. Two inputs were added upstream so this product could use them at all
(gitops-repo, and the CI bot identity) — before that, an Appsterisk deploy would have opened its
image-tag bump against Webhookr's gitops repository.
Carve-out: fork the four workflows into appsterisk-*. A day's work, no data.
Tailscale tailnet
The node joins the shared tailnet under tag:server, not a dedicated tag, because the ACL is a
tailnet-wide singleton owned by the Webhookr root. It isolates nothing today either — the policy
grants flat connectivity (src "*" → dst "*").
Carve-out: new tailnet, re-enroll the node, re-issue the auth key.
Cloudflare, GCP and Stripe accounts
Separate zones, buckets, projects and objects — but the same accounts. Account-level actions (billing, suspension, an owner losing access) reach both products.
Carve-out: new accounts; move the zone, copy R2 objects, migrate the GCP project, re-create the Stripe endpoint and re-issue its signing secret.
What is deliberately not shared
- No shared tables, no shared user table. Even if cross-product SSO ever arrives, domain data stays separate.
- No shared runtime dependency. Appsterisk calls nothing owned by Webhookr at request time. Routing Stripe webhooks through Webhookr was proposed and rejected: it would have put one product's uptime in the other's payment path, and Webhookr cannot forward the raw body or the signature header anyway, so signature verification was impossible through it.
- No shared credentials. Every token, key and secret is Appsterisk's own, including the ones that could technically have been reused.
3. External dependencies
| Service | What breaks without it |
|---|---|
| Neon | Everything |
| Cloudflare | DNS, TLS, CDN, the origin firewall's only permitted source |
| Hetzner | The cluster |
| Stripe | Purchases |
| Firebase / Identity Platform | Sign-in; token validation would keep working |
| GitHub | CI/CD and the ArgoCD source of truth |
| Cloudflare R2 | Avatars and claim documents |
| Redis Cloud | Nothing today |
4. Secrets, and where they live
| Secret | Held in | Notes |
|---|---|---|
ENCRYPTION_KEYS (KEK) | Cluster secret + appsterisk-infra/.env | Loss is unrecoverable — every sealed column becomes unreadable. Those two places are the same operator's control, so they are not a backup of each other. A third copy does not exist. See KEY-ROTATION.md. |
STRIPE_SECRET_KEY | Cluster secret | Runtime; can move money |
STRIPE_API_RESTRICTED_KEY | .env | Terraform only; products, prices, webhooks — cannot charge |
STRIPE_WEBHOOK_SECRET | Terraform output → cluster secret | The only thing separating a real payment event from a forged one |
DATABASE_URL | Cluster secret | sslmode=verify-full, stated explicitly because pg v9 will move require to weaker libpq semantics |
| R2 access key / secret | Cluster secret + .env | Minted by hand; the provider has no resource, and keys in state would be worse |
APPSTERISK_GITOPS_PAT | GitHub Actions secret, per repository | Not an org secret: an org secret with visibility: private reaches every private repo, which would hand Webhookr's CI a token that writes Appsterisk's desired state |
| ArgoCD deploy key | Cluster secret | Read-only, single repository |
| GHCR pull token | Cluster secret | read:packages only — cannot publish |
Nothing is committed. appsterisk-gitops holds desired state, never credentials; the three
bootstrap secrets are applied by hand and listed in bootstrap/README.md.
5. Carve-out procedure
Ordered so that nothing is unreachable partway through.
- Provision a GCP project, Cloudflare account, Hetzner project, Neon project, Redis Cloud account, Stripe account and Tailscale tailnet in the acquiring organization.
- Transfer the nine repositories. History and issues come with them.
- Fork the four shared workflows into
appsterisk-*and repoint the callers. - Move Terraform state: copy
gs://appsterisk-tfstateto the new project, updatebackend.tf,terraform init -migrate-state. Vendored modules need no changes. - Migrate the database:
pg_dumpfrom the Webhookr Neon project, restore into the new one, cut the connection string over. Requires downtime. - Copy R2 objects to the new account; re-point
R2_ENDPOINTand re-mint credentials. - Move the domain: transfer
appsterisk.meat the registrar, add the zone to the new Cloudflare account, re-issue the Origin CA certificate, re-publish the DS record. Expect a DNSSEC gap unless the DS is staged first. - Re-create Firebase: new project, new OAuth client, new IdP config. Existing users cannot be migrated as-is — the token issuer is the project, so every account must be re-linked or re-created. Plan this deliberately; it is the step most likely to be discovered late.
- Re-create Stripe: new account, new webhook endpoint, new signing secret. Past payment records stay in the old account.
- Re-issue every secret in §4. The KEK is the exception: it must be carried, not re-issued, or every sealed column is lost. Re-wrap it under a new KEK after the move.
- Re-enroll the node in the new tailnet.
- Verify against the acceptance journeys in
appsterisk-artifacts.
The two steps that are not mechanical are 5 (a real data migration) and 8 (identity cannot be transferred, only rebuilt). Everything else is moving a resource.