Skip to main content

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)

RepositoryRole
appsterisk-svcDomain, persistence, resolver, checkout, webhook. The only service with code today.
appsterisk-bffApplication API (GraphQL). Empty.
appsterisk-webPublic profile and owner dashboard. Empty.
appsterisk-adminAdmin console — its own repo so a build flag cannot expose it publicly. Empty.
appsterisk-appMobile (Expo). Empty.
appsterisk-docsThis document lives here.
appsterisk-infraTerraform root.
appsterisk-artifactsDecision records, ADRs, acceptance journeys.
appsterisk-gitopsArgoCD desired state.

Cloud resources — 73 in Terraform state

ProviderResources
GCPProject appsterisk-prd; state bucket gs://appsterisk-tfstate; Identity Platform config; Google sign-in IdP config
CloudflareZone appsterisk.me; DNSSEC; zone settings (Full strict, HSTS, TLS 1.2+); Origin CA certificate; 12 DNS records; R2 buckets appsterisk-assets and appsterisk-documents
HetznerServer appsterisk (cx23, nbg1); two persistent primary IPs; firewall restricting 80/443 to Cloudflare ranges
StripeWebhook endpoint for api.appsterisk.me/v1/stripe/webhook
GitHub9 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

ItemMonthly
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

ServiceWhat breaks without it
NeonEverything
CloudflareDNS, TLS, CDN, the origin firewall's only permitted source
HetznerThe cluster
StripePurchases
Firebase / Identity PlatformSign-in; token validation would keep working
GitHubCI/CD and the ArgoCD source of truth
Cloudflare R2Avatars and claim documents
Redis CloudNothing today

4. Secrets, and where they live

SecretHeld inNotes
ENCRYPTION_KEYS (KEK)Cluster secret + appsterisk-infra/.envLoss 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_KEYCluster secretRuntime; can move money
STRIPE_API_RESTRICTED_KEY.envTerraform only; products, prices, webhooks — cannot charge
STRIPE_WEBHOOK_SECRETTerraform output → cluster secretThe only thing separating a real payment event from a forged one
DATABASE_URLCluster secretsslmode=verify-full, stated explicitly because pg v9 will move require to weaker libpq semantics
R2 access key / secretCluster secret + .envMinted by hand; the provider has no resource, and keys in state would be worse
APPSTERISK_GITOPS_PATGitHub Actions secret, per repositoryNot 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 keyCluster secretRead-only, single repository
GHCR pull tokenCluster secretread: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.

  1. Provision a GCP project, Cloudflare account, Hetzner project, Neon project, Redis Cloud account, Stripe account and Tailscale tailnet in the acquiring organization.
  2. Transfer the nine repositories. History and issues come with them.
  3. Fork the four shared workflows into appsterisk-* and repoint the callers.
  4. Move Terraform state: copy gs://appsterisk-tfstate to the new project, update backend.tf, terraform init -migrate-state. Vendored modules need no changes.
  5. Migrate the database: pg_dump from the Webhookr Neon project, restore into the new one, cut the connection string over. Requires downtime.
  6. Copy R2 objects to the new account; re-point R2_ENDPOINT and re-mint credentials.
  7. Move the domain: transfer appsterisk.me at 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.
  8. 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.
  9. Re-create Stripe: new account, new webhook endpoint, new signing secret. Past payment records stay in the old account.
  10. 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.
  11. Re-enroll the node in the new tailnet.
  12. 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.