Heron Neobank
Monthly statements for 2M accounts without a Puppeteer cluster.
Neobank · Series C · 140 engineers
The build
Consumer neobank generating 2M statements/month. Replaced 40-node Puppeteer cluster with SnapPDF Enterprise. SOC 2 auditor signed off cleanly.
Stack
- · Kotlin
- · GCP
- · Kafka
- · Kubernetes
- · SnapPDF Enterprise
Architecture
Ops used: /merge → /watermark → /protect → /metadata
How they use SnapPDF
Heron Neobank serves 2M US consumers. Monthly statements go out on the 1st, quarterly tax forms on April 1 / Jan 31 — the pipeline must handle 2M PDFs in a 4-hour window without errors a regulator could cite.
The pre-SnapPDF stack: 40× c5.xlarge GCE instances running a custom Puppeteer-based renderer. Memory leaks caused weekly pod restarts. The ops team had a runbook titled "Statement Day" with 17 steps. A SOC 2 auditor flagged three findings related to the statement pipeline in the 2025 audit cycle.
The migration was staged over 12 weeks. Phase 1: shadow-mode, where both pipelines ran and outputs were byte-compared (allowing for acceptable timestamp drift). Phase 2: canary at 1% of customers. Phase 3: full cutover. Phase 4: decommission the Puppeteer cluster.
The new pipeline: each statement is rendered to page-PDFs from their Kotlin core-banking engine, SnapPDF merges them with the fee schedule + Reg E disclosure, watermark stamps the account's last-four for anti-fraud correlation, protect locks with AES-256 per-customer owner-password, metadata embeds SHA-256 hash + statement_id for audit reconciliation. The audit event goes to Kafka → their SIEM.
Measured: infrastructure cost for PDF generation fell from $456k/year to $0 (SnapPDF Enterprise absorbed). Engineering ownership fell from 2 FTE to 0.2 FTE. The 2026 SOC 2 audit had zero findings in the statement pipeline section; the auditor specifically noted the SHA-256-in-metadata pattern as an exemplar control.
The quiet win: the "Statement Day" runbook is gone. Monthly statements ship and nobody notices. Heron's VP Platform called it "the only migration that left no fingerprint."
Outcomes
Integration pattern
A simplified excerpt showing the core SnapPDF calls.
// heron-core/src/statement/StatementIssuer.kt
class StatementIssuer(
private val snap: SnapPDFClient,
private val vault: OwnerPasswordVault,
private val auditLog: KafkaProducer<String, AuditEvent>,
) {
suspend fun issue(account: Account, month: YearMonth): ByteArray {
val pages = renderStatementPages(account, month)
val merged = snap.merge(
MergeRequest(files = pages + listOf(FEE_SCHEDULE_URL, REG_E_URL))
)
val stamped = snap.watermark(
WatermarkRequest(
file = merged.pdf, kind = "text",
text = "ACCT ${account.lastFour}",
position = "bottom-center", opacity = 0.1
)
)
val locked = snap.protect(
ProtectRequest(
file = stamped.pdf,
ownerPassword = vault.key(account.id),
encryption = "aes-256",
permissions = Permissions(
printing = "high-res", copying = false, modifying = false
)
)
)
val hash = sha256(locked.pdf)
val final = snap.setMetadata(
SetMetadataRequest(
file = locked.pdf,
metadata = mapOf(
"title" to "Statement ${account.number} $month",
"subject" to "SHA256:$hash",
"keywords" to listOf("statement", account.id, month.toString())
)
)
)
auditLog.send("statement.issued", AuditEvent(account.id, month, hash))
return final.pdf
}
}Start building like this
Free tier gives you 100 ops/month — enough to prototype any of the flows on this page. No card required.