Saltar al contenido principal

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/pricing before 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)

ResponsibilitySupabase originalDO replacementWhy
Auth (JWT issue/verify)GoTrue + Supabase AuthCustom Fastify service on App Platform, jose lib, RS256 keys in Spaces-encrypted secretFull control of claims, key rotation, no per-MAU pricing
User/license DBSupabase PostgresDO Managed Postgres 1GBSame Postgres, managed, daily backups included
API / Edge FunctionsSupabase Edge Functions (Deno)Fastify on App Platform (Node 20)Single runtime, easier debug, no Deno lock-in
Object storageSupabase StorageDO Spaces (S3-compatible)Cheaper egress, CDN bundled, presigned URLs
CDNSupabase CDN (Cloudflare)Spaces CDN (built-in, Cloudflare-backed edge)One product, no extra wiring
Realtime (not needed)Supabase RealtimePolling every 6h/60s is fine
SecretsSupabase VaultApp Platform encrypted env vars + doctl for rotationNative, free, sufficient for solo dev
Email (password reset)Supabase Auth emailResend (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-xxs instance 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-prod with 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/ and apps/, private for manifests/ (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.digital on 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 /healthz every 60s, free tier covers 10 monitors.
  • Cost: $0.

8. Backups

  • Postgres: Daily automated backups (included) + weekly pg_dump to a separate Spaces bucket tedos-backups via 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 at https://api.tedos.tuempresa.digital/.well-known/jwks.json so 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) in refresh_tokens table.

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/refresh issues a new refresh token and marks the old one replaced_by.
  • Reuse detection: If a replaced_by token is presented again → revoke the entire chain for that user_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 reads licenses.status.

Rate limiting

  • @fastify/rate-limit with 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)

  1. GitHub Actions builds .dmg (macOS, code-signed + notarized) and .msi (Windows, code-signed).
  2. Tauri's tauri-build produces a .sig file using the updater private key (NOT the code-signing cert — separate concern).
  3. Action uploads {dmg, msi, sig} to s3://tedos-prod/shell/{version}/ via s3cmd with Spaces creds.
  4. Action calls POST /v1/internal/manifest/publish on the API with a short-lived GH OIDC token → API writes manifests/shell-latest.json to 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)
  • 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}/manifest on launch + every 6h. API returns the current pinned version per the user's entitlements and plan.
  • Versioning: latest, stable, beta channels 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 main touching api/**.
  • Action: digitalocean/app_action/deploy@v2 with app_name: tedos-api.
  • App Platform auto-rebuilds from the linked branch; the action just waits and reports status.

Artifact upload to Spaces

  • Use s3cmd (or aws-cli with --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

SecretPurpose
DIGITALOCEAN_ACCESS_TOKENdoctl + App Platform deploy action
DO_SPACES_KEY / DO_SPACES_SECRETUpload binaries
TAURI_UPDATER_PRIVATE_KEYSign shell update manifests
TAURI_UPDATER_PASSWORDDecrypt the key
APP_BUNDLE_PRIVATE_KEYSign micro-app bundles
APPLE_CERT_P12 / APPLE_CERT_PASSWORD / APPLE_ID / APPLE_TEAM_IDmacOS code-signing + notarization
WINDOWS_CERT_PFX / WINDOWS_CERT_PASSWORDWindows 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 doctl scripts 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 itemIdle (0)10 clients50 clients200 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

  1. brew install doctl terraform s3cmd and doctl auth init with a new PAT scoped to this project.
  2. Register tedos.tuempresa.digital (or chosen domain), point NS to Cloudflare, add an empty zone.
  3. Create Spaces bucket tedos-tfstate manually (chicken-and-egg for Terraform backend).
  4. cd infra && terraform init && terraform apply — provisions Postgres, Spaces, CDN, App Platform.
  5. Run initial Drizzle migrations against Postgres (drizzle-kit push:pg) — creates users, clients, licenses, refresh_tokens, audit_log.
  6. 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.
  7. Push API code to jeiemgi/tedos-api:main — App Platform auto-deploys.
  8. Configure custom domains: api.tedos.tuempresa.digital → App Platform, cdn.tedos.tuempresa.digital → Spaces CDN. Wait for cert issuance (~5 min).
  9. Build first signed shell release via GH Actions tag v0.1.0; verify the .dmg downloads from CDN and auto-updates against the manifest.
  10. Seed first client + license in Postgres via a one-off npm run seed:client -- --email me@x.com --plan starter.
  11. Install the shell on a clean macOS VM, log in with the seed account, confirm /v1/license/status returns active, and that a micro app bundle downloads + verifies.
  12. Set up Better Stack uptime monitor on /healthz and alert email → done.

Open Risks & Decisions Needed from José

  • Domain choice: tedos.tuempresa.digital vs tedos.io vs subdomain of tuempresa.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: nyc3 chosen for cost. If clients are MX-heavy, sfo3 adds ~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_log rows? 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? nyc3 is US-only. If yes, we need ams3 or fra1 and a routing layer.