Documentation

seal_api service design (Cloudflare)

Design document for seal_api β€” R2 ephemeral PDF staging, synchronous inline sign (~300ms), GCP KMS + SSL.com chain, RFC 3161 via rfc3161.ai.moda/adobe.

Edit on GitHub

This is the authoritative design document for seal_api β€” the HTTPS service at api.seal.club behind POST /v1/jobs/seal (Seal job).

Control plane (marketing site, /app dashboard, orders, API key UI) lives on Vercel and stays thin β€” see Platform overview β€” low-cost build.

seal_api optimizes for unique PDF in β†’ unique PDF out, short-lived R2 staging, and freight-first economics while keeping the public API shape stable.

Design goals

GoalImplication
Freight-first economicsPDF bodies live in R2; the API orchestrates with JSON + URLs, not multi‑MB proxies through app-host bandwidth (/plan freight table).
Ephemeral payloadsInputs and sealed outputs use lifecycle TTL; retention only where compliance requires it.
API compatibilityExisting multipart and source_url job shapes stay valid; add presigned PUT path as the scalable default.
Synchronous seal pathSigning tests show a full job completes reliably in ~300ms. Do it inline in the HTTP handler β€” no Cloudflare Queues, no separate executor Worker fleet, no second hop scheduling overhead.

Logical components

flowchart LR
  subgraph clients [Clients]
    C[Integrators / Hub]
  end
  subgraph edge [Cloudflare Worker β€” HTTP API]
    API[Auth + job ledger + inline seal]
  end
  subgraph data [Data plane]
    R2[(R2 buckets)]
  end
  subgraph crypto [Signing]
    KMS[GCP KMS β€” private key]
    TSA[rfc3161.ai.moda/adobe]
  end
  subgraph meta [Metadata - choose one]
    D1[(D1 / Postgres)]
  end
  C --> API
  API --> D1
  API --> R2
  API --> KMS
  API --> TSA
  API --> C
ComponentResponsibility
HTTP API WorkerBearer auth, Idempotency-Key, quota, create/patch Job rows, stream PDF β†’ KMS sign β†’ TSA gateway β†’ artifact, return completed receipt (~300ms).
R2Byte storage only: raw uploads, sealed PDFs, optional virus-scan quarantine prefix. No long-term archive by default.
Job ledgerD1, Hyperdrive→Postgres, or external DB: phases, R2 keys, checksums, errors — not the PDF itself.
Webhook deliveryOptional fire-and-forget POST on terminal states via waitUntil (see Webhooks) β€” not a separate queue consumer.

Bucket layout (single bucket + prefixes is fine):

PrefixContentsTypical TTL
ingest/{tenant}/{job}/input.pdfVerified upload bytesDelete after seal success or failure + quarantine window
artifacts/{tenant}/{job}/sealed.pdfCustomer-facing sealed fileDelete after download or 24–72h unless compliance retention
quarantine/{tenant}/{job}/…Positive virus scan / policy blockShort retention for audit export only

Best for large PDFs and high volume: upload bytes go direct to R2; the complete-upload call runs inline sign (~300ms).

sequenceDiagram
  participant Client
  participant API as HTTP API Worker
  participant R2 as R2
  participant KMS as GCP KMS
  participant TSA as rfc3161.ai.moda/adobe

  Client->>API: POST /v1/jobs/seal (JSON: checksum, request_upload)
  API->>API: Auth + idempotency + create Job (awaiting_upload)
  API->>R2: Presigned PUT URL (short TTL)
  API-->>Client: job_id + upload instructions (URL + headers)
  Client->>R2: PUT PDF bytes (direct to R2)
  Client->>API: POST /v1/jobs/{id}/complete-upload (illustrative)
  API->>R2: HEAD/GET β€” verify size/hash
  API->>R2: GET input (same-fabric)
  API->>KMS: Sign PDF digest (key never leaves KMS)
  API->>TSA: RFC 3161 timestamp (adobe gateway)
  API->>R2: PUT artifacts/.../sealed.pdf
  API->>API: Job phase = completed + receipt digest
  API-->>Client: 200 receipt + presigned GET (or download URL)

Sequence β€” multipart compatibility (file=@doc.pdf)

For small files or legacy clients: stream file from the HTTP request into R2 using streaming uploads, then inline sign in the same request before returning.

sequenceDiagram
  participant Client
  participant API as HTTP API Worker
  participant R2 as R2
  participant KMS as GCP KMS
  participant TSA as rfc3161.ai.moda/adobe

  Client->>API: multipart POST /v1/jobs/seal
  API->>R2: Stream request body β†’ ingest/.../input.pdf
  API->>R2: GET input (same Worker, same request)
  API->>KMS: Sign PDF digest
  API->>TSA: RFC 3161 timestamp
  API->>R2: PUT artifacts/.../sealed.pdf
  API-->>Client: 200 completed job + download URL (~300ms)

Sequence β€” source_url ingestion

When the JSON body carries source_url (Seal job):

  1. Defensive HEAD, optional range GET for magic bytes (same Worker; no customer credential reuse).
  2. Stream download directly into R2 ingest/… (avoid holding full blob on heap).
  3. Verify checksum_sha256 if supplied β†’ inline sign in the same request.

Treat hostile URLs with timeouts, size caps, and SSRF controls (allow-lists or egress policies).

Signing stack

Runtime sealing uses GCP KMS for the private key and a single RFC 3161 gateway for timestamps. SSL.com is the certificate authority (org document-signing seat β€” /plan); it is not on the hot-path API for every seal.

Document signature (GCP KMS + SSL.com chain)

  1. A GCP KMS asymmetric key holds the signing private key (non-exportable).
  2. The Worker generated a CSR from that key; SSL.com issued the org document-signing certificate against it.
  3. The issued certificate + chain are configured on seal_api (env/secret per tenant or platform key).
  4. On each job, the Worker: builds the PDF ByteRange digest β†’ calls projects/…/cryptoKeys/…/cryptoKeyVersions/…:asymmetricSign on KMS β†’ embeds the CMS/PAdES signature using the SSL.com-issued leaf + intermediates.

The private key never transits the Worker heap as PEM/PKCS#8 β€” only digests and returned signatures cross the wire.

Timestamp (RFC 3161 gateway)

All TSA traffic goes through one HTTPS gateway:

https://rfc3161.ai.moda/adobe

The Worker posts the message imprint (digest + algorithm OID) to that endpoint, receives a RFC 3161 time-stamp token, and embeds it in the PDF signature (/SubFilter + DSS if required). No direct calls to disparate TSA endpoints per job β€” one gateway, one integration surface.

When async mode still exists

The public Seal job surface may document mode=async (202 + poll/webhook) for integrators that cannot hold a TLS connection open. Implementation does not require Queues: the same handler can return early and finish via waitUntil, or async can remain a compat shim while blocking / default sync is the production path.

At ~300ms p95, prefer synchronous completion β€” simpler billing, fewer moving parts, one Worker invocation per seal (presign path: one extra lightweight call for upload instructions).

Signing & receipts

  • The handler produces a receipt manifest (hashes, KMS key version id, cert chain refs, TSA policy id) stored in the job ledger β€” aligned with API overview (Receipt resource).
  • Document signature: GCP KMS asymmetricSign with the SSL.com-issued chain attached to the platform (or tenant) KMS key.
  • Timestamp: RFC 3161 token from https://rfc3161.ai.moda/adobe embedded in the sealed PDF.
  • Return to clients: job_id, phase: completed, presigned GET for artifacts/…/sealed.pdf with short TTL and one-time token if needed.

Lifecycle & cleanup

  • R2 Object Lifecycle rules delete ingest/* after N hours regardless (safety net).
  • On successful download of sealed PDF, optionally delete artifact immediately (privacy-by-design).
  • Failed jobs: purge input after forensic window; store only error code + trace in ledger.

Security controls

RiskMitigation
Bucket enumerationNo public bucket ACLs; access via presigned URLs or Worker-only R2 binding.
Stale presigned URLsTTL 5–15 minutes for PUT; separate GET TTL for download.
SSRF (source_url)Egress allow-list, block RFC1918/link-local, strict timeouts and byte caps.
Quota drainPer-token rate limits before presign; Idempotency-Key dedupe at API layer.
Tenant isolationKey prefixes tenants/{tenant_id}/; Worker enforces tenant from auth claims only.
Handler CPU ceilingTune max PDF size to stay under Worker limits; ~300ms target assumes typical org PDFs at tested sizes.
KMS credential scopeWorker uses a least-privilege GCP service account / WIF: cloudkms.cryptoKeyVersions.useToSign on the sealing key only.
TSA gateway availabilityTimeouts + retry once on rfc3161.ai.moda/adobe; fail job with tsa_unavailable if token missing (do not ship half-stamped PDFs).

Mapping to the Vercel reseller hub

The reseller hub on Vercel forwards validated jobs to POST /v1/jobs/seal via SEAL_API_BASE_URL β€” see Platform overview. Under this architecture:

  • Hub may continue multipart β†’ API Worker streams into R2 and waits for inline completion (~300ms).
  • Preferred: hub requests presigned upload from seal_api and returns URLs to the browser β€” zero PDF bytes through Vercel.

Implementation choices (pick one stack)

ConcernChosen stack
Job ledgerD1 now; Hyperdrive→Postgres later if Neon becomes source of truth
Private keyGCP KMS asymmetric key (CSR β†’ SSL.com issue β†’ chain stored on seal_api)
PDF crypto in WorkerStream ByteRange, orchestrate KMS + TSA HTTP; no exported private key material
Timestamphttps://rfc3161.ai.moda/adobe for all RFC 3161 ops
ObservabilityWorkers Logs + traces; log Seal-Trace-Id, KMS key version, TSA latency

Cost estimation (order-of-magnitude)

All figures are napkin math for capacity planning β€” reconcile against your Cloudflare invoice and actual p95 seal latency (~300ms in signing tests). Signing vendors, certs, Neon/Postgres, and observability sit outside this table.

Reference workload

Align with the stress scenario used on Business plan β€” freight table:

  • ~30 million seals/month (e.g. 1β€―000 orgs Γ— 1β€―000 PDFs/day Γ— 30 days)
  • ~1 MB average payload
  • Ephemeral R2: inputs and sealed outputs deleted on schedule (average hot footprint ~0.5–2 TB-month, not 30 TB archived)
  • One synchronous API invocation per seal (presign flow: +1 lightweight call for upload instructions β†’ model ~45M invocations/mo)

Cloudflare line items (list-style retail)

Pricing anchors: R2, Workers Paid.

Line itemModeling assumptionsEst. USD/month
R2 Class A~60M writes/month (client ingests ~30M + handler writes sealed ~30M); first 1M/mo included~$265
R2 Class B~50M reads/month (handler GET input + verification HEAD/range); first 10M/mo included~$14
R2 storage~0.5–2 TB average retained inside lifecycle windows Γ— ~$0.015/GB-mo (standard class at publish time)~$8–30
Workers β€” requests~45M invocations/month (sync seal + presign setup); first 10M included on Paid plan + ~$0.30/extra million~$16 + $5 plan floor
Workers β€” CPU time~300ms average CPU-ms per seal (signing test envelope) Γ— 30M seals β‰ˆ 9B CPU-ms/mo β€” first 30M CPU-ms/mo included, then ~$0.02/extra million CPU-ms~$179

Illustrative total (Cloudflare-shaped infrastructure only): ~$490–520/mo (includes ~$8–30 storage band).

Removed vs queue design: no Queues line (~$36/mo saved), fewer Worker request doublings (no consumer invocations), and no queue poison/retry multiplier.

Why this beats naive full-proxy hosting

For the same ~30 TB/month of customer-visible PDF bytes, piping bodies through a premium app host often lands in the ~$2.6k–5k+ (S3/GCS-style internet egress) or ~$8k–20k+ (naive Vercel Fast Data Transfer envelope) bands summarized on /plan. Here, PDF bytes stay on R2; the API moves metadata except during short inline read/sign/write; R2 egress to the internet is not billed the way hyperscaler object egress is β€” customer download is usually a presigned GET from R2 (see R2 pricing).

Excluded costs (still real money)

BucketExamples
PKI / signingSSL.com org cert seats (~$209/yr), GCP KMS sign ops (TSA via rfc3161.ai.moda is free β€” no per-stamp fee while that router holds)
Ledger DBNeon/D1 rows, Hyperdrive, backups
Egress outside CFWebhooks to slow peers, source_url fetch from third-party origins
People & supportIf you hire beyond automation-first posture