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.
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
| Goal | Implication |
|---|---|
| Freight-first economics | PDF bodies live in R2; the API orchestrates with JSON + URLs, not multiβMB proxies through app-host bandwidth (/plan freight table). |
| Ephemeral payloads | Inputs and sealed outputs use lifecycle TTL; retention only where compliance requires it. |
| API compatibility | Existing multipart and source_url job shapes stay valid; add presigned PUT path as the scalable default. |
| Synchronous seal path | Signing 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 | Component | Responsibility |
|---|---|
| HTTP API Worker | Bearer auth, Idempotency-Key, quota, create/patch Job rows, stream PDF β KMS sign β TSA gateway β artifact, return completed receipt (~300ms). |
| R2 | Byte storage only: raw uploads, sealed PDFs, optional virus-scan quarantine prefix. No long-term archive by default. |
| Job ledger | D1, HyperdriveβPostgres, or external DB: phases, R2 keys, checksums, errors β not the PDF itself. |
| Webhook delivery | Optional fire-and-forget POST on terminal states via waitUntil (see Webhooks) β not a separate queue consumer. |
Bucket layout (single bucket + prefixes is fine):
| Prefix | Contents | Typical TTL |
|---|---|---|
ingest/{tenant}/{job}/input.pdf | Verified upload bytes | Delete after seal success or failure + quarantine window |
artifacts/{tenant}/{job}/sealed.pdf | Customer-facing sealed file | Delete after download or 24β72h unless compliance retention |
quarantine/{tenant}/{job}/β¦ | Positive virus scan / policy block | Short retention for audit export only |
Sequence β presigned upload (recommended)
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):
- Defensive HEAD, optional range GET for magic bytes (same Worker; no customer credential reuse).
- Stream download directly into R2
ingest/β¦(avoid holding full blob on heap). - Verify
checksum_sha256if 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)
- A GCP KMS asymmetric key holds the signing private key (non-exportable).
- The Worker generated a CSR from that key; SSL.com issued the org document-signing certificate against it.
- The issued certificate + chain are configured on seal_api (env/secret per tenant or platform key).
- On each job, the Worker: builds the PDF
ByteRangedigest β callsprojects/β¦/cryptoKeys/β¦/cryptoKeyVersions/β¦:asymmetricSignon 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 (
Receiptresource). - Document signature: GCP KMS
asymmetricSignwith the SSL.com-issued chain attached to the platform (or tenant) KMS key. - Timestamp: RFC 3161 token from
https://rfc3161.ai.moda/adobeembedded in the sealed PDF. - Return to clients:
job_id,phase: completed, presigned GET forartifacts/β¦/sealed.pdfwith 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
| Risk | Mitigation |
|---|---|
| Bucket enumeration | No public bucket ACLs; access via presigned URLs or Worker-only R2 binding. |
| Stale presigned URLs | TTL 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 drain | Per-token rate limits before presign; Idempotency-Key dedupe at API layer. |
| Tenant isolation | Key prefixes tenants/{tenant_id}/; Worker enforces tenant from auth claims only. |
| Handler CPU ceiling | Tune max PDF size to stay under Worker limits; ~300ms target assumes typical org PDFs at tested sizes. |
| KMS credential scope | Worker uses a least-privilege GCP service account / WIF: cloudkms.cryptoKeyVersions.useToSign on the sealing key only. |
| TSA gateway availability | Timeouts + 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)
| Concern | Chosen stack |
|---|---|
| Job ledger | D1 now; HyperdriveβPostgres later if Neon becomes source of truth |
| Private key | GCP KMS asymmetric key (CSR β SSL.com issue β chain stored on seal_api) |
| PDF crypto in Worker | Stream ByteRange, orchestrate KMS + TSA HTTP; no exported private key material |
| Timestamp | https://rfc3161.ai.moda/adobe for all RFC 3161 ops |
| Observability | Workers 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 item | Modeling assumptions | Est. 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)
| Bucket | Examples |
|---|---|
| PKI / signing | SSL.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 DB | Neon/D1 rows, Hyperdrive, backups |
| Egress outside CF | Webhooks to slow peers, source_url fetch from third-party origins |
| People & support | If you hire beyond automation-first posture |
Related docs
- Platform overview β low-cost build β Vercel control plane vs seal_api split
- Seal job β
POST /v1/jobs/seal - API overview
- Integration checklist