Tu Empresa Digital OS — DigitalOcean Implementation Plan
:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::
historical· devops · updated 2026-06-26 · source
HISTORICAL (2026-06-26). DigitalOcean was never the live host — superseded. The live deployment is Vercel (admin) + Fly (
tedos-api, web + worker, Fly Managed Postgres); see orientation.md System status and decisions/log.md. Kept for context only.
Pricing note: WebSearch/WebFetch were unavailable during drafting. All USD figures are based on publicly known DigitalOcean list prices as of late 2025 and should be re-verified against
digitalocean.com/pricingbefore signing off. Margins of ±10% are expected.
TL;DR
- Stack: API on App Platform (Basic) + Managed Postgres (Basic 1GB) + Spaces with built-in CDN for binaries/bundles + Cloudflare DNS in front of an apex domain.
- Idle cost (0–1 clients): ~$29/mo (App Platform $5 + Postgres $15 + Spaces $5 + DNS free + monitoring free tier).
- At 50 clients: ~$47–60/mo depending on bandwidth (assume ~150 GB/mo egress at $0.01/GB beyond the 1 TB included = negligible; main bump is one extra App Platform instance for HA = +$5).
- Biggest risks: (1) Tauri updater signing key custody — losing it bricks the auto-update path; (2) DB single point of failure on Basic tier (no HA replica until $60/mo Professional); (3) cold-start on Spaces CDN for first global client of the day (acceptable).
- Winner over Supabase: ~50% cheaper at idle vs Supabase Pro ($25 + Auth add-ons + Storage egress fees), full control of JWT keys, no vendor lock for Auth.
Service Map (Supabase → DO)
| Responsibility | Supabase original | DO replacement | Why |
|---|---|---|---|
| Auth (JWT issue/verify) | GoTrue + Supabase Auth | Custom Fastify service on App Platform, jose lib, RS256 keys in Spaces-encrypted secret | Full control of claims, key rotation, no per-MAU pricing |
| User/license DB | Supabase Postgres | DO Managed Postgres 1GB | Same Postgres, managed, daily backups included |
| API / Edge Functions | Supabase Edge Functions (Deno) | Fastify on App Platform (Node 20) | Single runtime, easier debug, no Deno lock-in |
| Object storage | Supabase Storage | DO Spaces (S3-compatible) | Cheaper egress, CDN bundled, presigned URLs |
| CDN | Supabase CDN (Cloudflare) | Spaces CDN (built-in, Cloudflare-backed edge) | One product, no extra wiring |
| Realtime (not needed) | Supabase Realtime | — | Polling every 6h/60s is fine |
| Secrets | Supabase Vault | App Platform encrypted env vars + doctl for rotation | Native, free, sufficient for solo dev |
| Email (password reset) | Supabase Auth email | Resend (external, free tier 3k/mo) | Cheaper and better deliverability than self-hosting |
Architecture Diagram (ASCII)
┌────────────────────────┐
│ Tauri Desktop Shell │
│ (macOS / Windows) │
└───────────┬────────────┘
│ HTTPS (JWT)
▼
┌──────────────────────────────────┐
│ api.tedos.tuempresa.digital │
│ DO App Platform (Fastify/Node) │
│ /auth /licenses /entitlements │
│ /telemetry /updates/manifest │
└──┬──────────────┬────────────┬───┘
│ │ │
SQL │ S3 API │ issue │ signed URL
▼ ▼ ▼
┌────────────────┐ ┌──────────────────────────┐
│ Managed │ │ Spaces + CDN │
│ Postgres (1GB) │ │ /shell/{ver}/*.dmg .msi │
│ users │ │ /apps/{slug}/{ver}/* │
│ clients │ │ /manifests/*.json (sig) │
│ licenses │ └──────────────┬───────────┘
│ refresh_tokens│ │ CDN edge
│ audit_log │ ▼
└────────────────┘ Client downloads
Components
1. API runtime — App Platform (Basic, 1× $5 instance)
- Product: DO App Platform, Web Service,
basic-xxs(512 MB / 1 vCPU). - Runtime: Node 20, Fastify (lighter than Express, native schema validation via
@sinclair/typebox). - Why not Functions: Cold starts hurt the 60-second payment recheck loop; long-lived connections to Postgres benefit from a warm process. Also cheaper at our scale.
- Why not Droplet: No patching, no nginx, no systemd. App Platform handles TLS, deploys, rollbacks.
- Alternatives considered:
- Hono on Functions — rejected (cold start + harder pg-pool reuse).
- Bun on Droplet — rejected (ops burden for solo dev).
- Scale plan: Add a second
basic-xxsinstance at 25+ clients (+$5). - Cost: $5/mo.
2. Database — DO Managed Postgres 16, Basic 1GB / 1 vCPU
- Tier:
db-s-1vcpu-1gb, single node, daily backups, 7-day PITR. - Connection pooling: Enable built-in PgBouncer in transaction mode (free, mandatory on App Platform).
- Migrations:
drizzle-kit(lighter than Prisma, no migration daemon). - Why not Supabase free Postgres: Need to own the auth schema and keys.
- Upgrade path: Bump to 2GB ($30) at ~200 clients, add standby replica ($60) at ~500.
- Cost: $15/mo.
3. Object storage — DO Spaces (1 bucket, multi-prefix)
- One bucket
tedos-prodwith prefixes:shell/{platform}/{version}/TeDOS.{dmg|msi|exe.sig}apps/{slug}/{version}/bundle.tar.gz(+.sig)manifests/shell-latest.json,manifests/apps/{slug}.json
- Region:
nyc3(closest to Mexico City + cheapest egress). - Access: Public-read for
shell/andapps/, private formanifests/(served via API-signed URLs for tamper protection, OR public + signature inside file — see Updater section). - Cost: $5/mo (250 GB storage + 1 TB outbound included).
4. CDN — Spaces CDN (built-in)
- Enable the CDN toggle on the Space, attach a custom subdomain
cdn.tedos.tuempresa.digital. - TLS auto-provisioned via Let's Encrypt through DO.
- Why not separate Cloudflare R2 + Workers: One bill, one IAM, no cross-vendor egress surprises. Spaces CDN already uses a global edge.
- Cost: $0 (included).
5. DNS + TLS — Cloudflare DNS (free) + DO-issued certs
- Apex
tedos.tuempresa.digitalon Cloudflare (better dashboard, free DNSSEC, easy email records). CNAME api.tedos.tuempresa.digital → <app-platform-default>.ondigitalocean.app— TLS handled by App Platform.CNAME cdn.tedos.tuempresa.digital → <space>.<region>.cdn.digitaloceanspaces.com— TLS handled by Spaces CDN.- Cost: $0.
6. Secrets management — App Platform encrypted env vars (Phase 1)
- All secrets (JWT signing keys, Spaces access keys, Resend API key, DB URL is auto-injected) live as encrypted env vars in the App Platform app spec.
- Rotation via
doctl apps update --spec app.yaml. - Phase 2 (when we hit 100+ clients or audit pressure): Move signing keys to a small Vault dev-mode Droplet ($6/mo) — out of scope for v1.
- Cost: $0.
7. Logging / metrics — DO Monitoring + Better Stack (free tier)
- DO Monitoring: CPU/memory/disk, free, native.
- Logs: App Platform stdout → forward to Better Stack Logs (free tier 1 GB/30 days). Fastify →
pino→ log drain. - Uptime: Better Stack Uptime monitors hitting
/healthzevery 60s, free tier covers 10 monitors. - Cost: $0.
8. Backups
- Postgres: Daily automated backups (included) + weekly
pg_dumpto a separate Spaces buckettedos-backupsvia a scheduled App Platform Worker (cron). - Spaces: Versioning ON for
manifests/prefix; the binaries themselves are reproducible from GitHub release tags so no separate backup needed. - Cost: $0 (Postgres backups included, backup Space within free tier).
Auth Implementation
JWT issuance
- Library:
jose(modern, audited, RS256 support). - Algorithm: RS256 — public key shipped in Tauri shell binary for offline verification during the 72h grace window; private key on server only.
- Key rotation: Two-key window (
kid: 2026-q2,kid: 2026-q3). New tokens signed with current key; verifier accepts both for 90 days post-rotation. JWKS exposed athttps://api.tedos.tuempresa.digital/.well-known/jwks.jsonso future clients can fetch fresh keys. - Access token TTL: 1h. Claims:
sub(user_id),cid(client_id),plan,entitlements: [app_slug],iat,exp,kid. - Refresh token TTL: 30d. Opaque (32-byte random), stored hashed (
argon2id) inrefresh_tokenstable.
Refresh token storage & revocation
create table refresh_tokens (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
token_hash text not null, -- argon2id(token)
device_fp text, -- Tauri-reported fingerprint
issued_at timestamptz not null default now(),
expires_at timestamptz not null,
revoked_at timestamptz,
replaced_by uuid references refresh_tokens(id)
);
create index on refresh_tokens (user_id) where revoked_at is null;
- Rotation on use: Every
/auth/refreshissues a new refresh token and marks the old onereplaced_by. - Reuse detection: If a
replaced_bytoken is presented again → revoke the entire chain for thatuser_id(token theft signal). - Admin revocation:
update refresh_tokens set revoked_at = now() where user_id = $1— instant kill switch.
Suspension / entitlement endpoints
GET /v1/license/status
→ 200 { status: "active"|"grace"|"suspended", plan, entitlements: [...], grace_until?: iso8601 }
POST /v1/auth/login { email, password } → { access, refresh, expires_in }
POST /v1/auth/refresh { refresh } → { access, refresh, expires_in }
POST /v1/auth/password/reset { email } → 204
GET /v1/updates/manifest?ch=stable&os=darwin → signed Tauri manifest
GET /v1/apps/{slug}/manifest → { version, url, sig, sha256 }
POST /v1/telemetry { events: [...] } → 202
- Suspension logic: A nightly cron in the API checks Stripe subscription status → updates
licenses.status. The 6h client poll just readslicenses.status.
Rate limiting
@fastify/rate-limitwith Postgres-backed store (or Upstash Redis if we add it later).- Per-IP: 60 req/min on
/auth/*, 600 req/min on/v1/license/status(50 clients × 4 polls/day = trivial). - DDoS: Cloudflare proxy ON for
api.tedos.tuempresa.digital(orange cloud) — free L7 protection.
Updater Pipeline
Shell artifacts (Tauri auto-updater)
- GitHub Actions builds
.dmg(macOS, code-signed + notarized) and.msi(Windows, code-signed). - Tauri's
tauri-buildproduces a.sigfile using the updater private key (NOT the code-signing cert — separate concern). - Action uploads
{dmg, msi, sig}tos3://tedos-prod/shell/{version}/vias3cmdwith Spaces creds. - Action calls
POST /v1/internal/manifest/publishon the API with a short-lived GH OIDC token → API writesmanifests/shell-latest.jsonto Spaces and purges CDN cache for that key.
Manifest signing
- Tauri updater pubkey lives in
tauri.conf.json(shipped in the binary). The private key lives ONLY in:- GitHub Actions secret
TAURI_UPDATER_PRIVATE_KEY(for CI signing) - José's 1Password vault (for recovery)
- GitHub Actions secret
- Manifest format (Tauri v2):
{"version": "1.4.2","pub_date": "2026-05-19T12:00:00Z","platforms": {"darwin-aarch64": {"signature": "...","url": "https://cdn.tedos.tuempresa.digital/shell/1.4.2/TeDOS.app.tar.gz"},"windows-x86_64": {"signature": "...","url": "https://cdn.tedos.tuempresa.digital/shell/1.4.2/TeDOS.msi.zip"}}}
Micro app bundles
- Each micro app has its own semver, stored at
apps/{slug}/{version}/bundle.tar.gz+.sig. - Bundle signing uses a second updater key pair (different from shell key — blast radius isolation). Public key shipped in shell, used to verify bundle sig before extracting.
- Shell calls
GET /v1/apps/{slug}/manifeston launch + every 6h. API returns the current pinned version per the user'sentitlementsandplan. - Versioning:
latest,stable,betachannels per app — stored in DB, swappable without redeploying API.
Cache invalidation
- Spaces CDN purge via
doctl compute cdn flush <cdn-id> --files /manifests/*after each publish. - Cache-Control:
/shell/*and/apps/*binaries →public, max-age=31536000, immutable(versioned URLs, never change)./manifests/*→public, max-age=60, stale-while-revalidate=300(allow 1 min staleness, fine for 6h poll).
CI/CD
GitHub Actions workflows
.github/workflows/
api-deploy.yml # on push to main, paths: api/**
shell-release.yml # on tag v*, builds macOS+Windows, uploads to Spaces
app-release.yml # on tag app/{slug}/v*, builds bundle, uploads to Spaces
API deploy
- Trigger: push to
maintouchingapi/**. - Action:
digitalocean/app_action/deploy@v2withapp_name: tedos-api. - App Platform auto-rebuilds from the linked branch; the action just waits and reports status.
Artifact upload to Spaces
- Use
s3cmd(oraws-cliwith--endpoint-url) — Spaces is S3-compatible. - Presigned URLs not needed for binaries (public-read), but used for
manifests/if we go the private-manifest route.
GitHub secrets required
| Secret | Purpose |
|---|---|
DIGITALOCEAN_ACCESS_TOKEN | doctl + App Platform deploy action |
DO_SPACES_KEY / DO_SPACES_SECRET | Upload binaries |
TAURI_UPDATER_PRIVATE_KEY | Sign shell update manifests |
TAURI_UPDATER_PASSWORD | Decrypt the key |
APP_BUNDLE_PRIVATE_KEY | Sign micro-app bundles |
APPLE_CERT_P12 / APPLE_CERT_PASSWORD / APPLE_ID / APPLE_TEAM_ID | macOS code-signing + notarization |
WINDOWS_CERT_PFX / WINDOWS_CERT_PASSWORD | Windows Authenticode |
RESEND_API_KEY | (only if email is sent from CI; normally API has it) |
IaC
Recommendation: Terraform.
Justification:
- DO has a mature, first-party Terraform provider (
digitalocean/digitalocean). - Pulumi adds a TS toolchain dependency we don't need outside the IaC repo.
- Plain
doctlscripts don't track state — drift becomes invisible at month 6. - Terraform state stored in Spaces with native S3 backend (no Terraform Cloud needed).
Starter snippet (infra/main.tf)
terraform {
required_version = ">= 1.7"
required_providers {
digitalocean = { source = "digitalocean/digitalocean", version = "~> 2.40" }
}
backend "s3" {
endpoint = "nyc3.digitaloceanspaces.com"
bucket = "tedos-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1" # required dummy, ignored by Spaces
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
}
}
variable "do_token" { type = string, sensitive = true }
provider "digitalocean" { token = var.do_token }
# --- Database ---
resource "digitalocean_database_cluster" "pg" {
name = "tedos-pg"
engine = "pg"
version = "16"
size = "db-s-1vcpu-1gb"
region = "nyc3"
node_count = 1
}
# --- Spaces (binaries + CDN) ---
resource "digitalocean_spaces_bucket" "assets" {
name = "tedos-prod"
region = "nyc3"
acl = "private"
versioning { enabled = true }
}
resource "digitalocean_cdn" "assets_cdn" {
origin = digitalocean_spaces_bucket.assets.bucket_domain_name
custom_domain = "cdn.tedos.tuempresa.digital"
certificate_name = digitalocean_certificate.cdn.name
ttl = 3600
}
# --- App Platform ---
resource "digitalocean_app" "api" {
spec {
name = "tedos-api"
region = "nyc"
service {
name = "api"
instance_size_slug = "basic-xxs"
instance_count = 1
http_port = 8080
github {
repo = "jeiemgi/tedos-api"
branch = "main"
deploy_on_push = true
}
env { key = "DATABASE_URL" value = digitalocean_database_cluster.pg.private_uri type = "SECRET" }
env { key = "JWT_PRIVATE_KEY" value = var.jwt_private_key type = "SECRET" }
env { key = "SPACES_KEY" value = var.spaces_key type = "SECRET" }
env { key = "SPACES_SECRET" value = var.spaces_secret type = "SECRET" }
env { key = "RESEND_API_KEY" value = var.resend_api_key type = "SECRET" }
health_check { http_path = "/healthz" }
}
}
}
Cost Estimate
| Line item | Idle (0) | 10 clients | 50 clients | 200 clients |
|---|---|---|---|---|
| App Platform (Basic xxs) | $5 | $5 | $10 (2×) | $24 (Pro xs) |
| Managed Postgres 1GB | $15 | $15 | $15 | $30 (2GB) |
| Spaces + CDN (250 GB / 1 TB) | $5 | $5 | $5 | $5 |
| Spaces overage (egress) | $0 | $0 | ~$2 | ~$10 |
| Backups Space | $0 | $0 | $0 | $5 |
| DNS (Cloudflare) | $0 | $0 | $0 | $0 |
| Logs (Better Stack free) | $0 | $0 | $0 | $0 |
| Email (Resend free → paid) | $0 | $0 | $0 | $20 |
| Total DO stack | $25 | $25 | $32 | $94 |
| Supabase Pro comparison | $25 | $25 | $25 + auth add-ons ~$10 | $25 + add-ons + egress ~$60 |
Verdict: DO is marginally cheaper at idle, slightly cheaper at scale, and much cheaper if egress grows (Spaces' 1 TB included beats Supabase's metered egress). The real win is control, not cost.
Migration / Rollout Steps
brew install doctl terraform s3cmdanddoctl auth initwith a new PAT scoped to this project.- Register
tedos.tuempresa.digital(or chosen domain), point NS to Cloudflare, add an empty zone. - Create Spaces bucket
tedos-tfstatemanually (chicken-and-egg for Terraform backend). cd infra && terraform init && terraform apply— provisions Postgres, Spaces, CDN, App Platform.- Run initial Drizzle migrations against Postgres (
drizzle-kit push:pg) — createsusers,clients,licenses,refresh_tokens,audit_log. - Generate RS256 JWT keypair (
openssl genpkey -algorithm RSA -out jwt.pem -pkeyopt rsa_keygen_bits:2048) and Tauri updater keypair (tauri signer generate). Store private keys in 1Password + GH secrets; public keys in repo. - Push API code to
jeiemgi/tedos-api:main— App Platform auto-deploys. - Configure custom domains:
api.tedos.tuempresa.digital→ App Platform,cdn.tedos.tuempresa.digital→ Spaces CDN. Wait for cert issuance (~5 min). - Build first signed shell release via GH Actions tag
v0.1.0; verify the.dmgdownloads from CDN and auto-updates against the manifest. - Seed first client + license in Postgres via a one-off
npm run seed:client -- --email me@x.com --plan starter. - Install the shell on a clean macOS VM, log in with the seed account, confirm
/v1/license/statusreturnsactive, and that a micro app bundle downloads + verifies. - Set up Better Stack uptime monitor on
/healthzand alert email → done.
Open Risks & Decisions Needed from José
- Domain choice:
tedos.tuempresa.digitalvstedos.iovs subdomain oftuempresa.digital. Affects DNS + cert SANs. - Stripe vs Mercado Pago for billing: Stripe assumed throughout — confirm. Mercado Pago is more friction but better for MX B2B clients who refuse USD card payments.
- Region:
nyc3chosen for cost. If clients are MX-heavy,sfo3adds ~10ms but is the same price. Confirm latency requirement. - Tauri updater key custody: Need a documented recovery process (1Password shared vault? printed paper backup in a safe?). Losing this key = need to push a new shell with a new pubkey to every client manually.
- Code-signing certs: Apple Developer ($99/yr) and Windows EV cert (~$300/yr from SSL.com) are not in the cost table above — they're prerequisites, not DO costs. Confirm budget.
- Telemetry retention: How long do we keep
audit_logrows? Affects DB size and the 1GB Postgres ceiling timing. Default: 90 days, prune nightly. - Password reset email sender domain: Need to set up SPF/DKIM/DMARC on the chosen apex domain for Resend. Half-day of DNS work.
- HA Postgres: Single-node is fine for v1, but if a client's payroll runs on this and DB goes down for 30 min during a DO incident, that's bad PR. Decide threshold for upgrading to replica ($60/mo).
- GDPR / data residency: Any EU clients in pipeline?
nyc3is US-only. If yes, we needams3orfra1and a routing layer.