Disclosure: I created this article for the purpose of entering the H0 Hackathon, Hack the Zero Stack with Vercel v0 and AWS Databases.
#H0Hackathon
🔗 Live app: https://custody-zeta.vercel.app
The gap I wanted to close
When a parent revokes consent or sets a spending cap for a child on a global platform, that decision may not take effect everywhere at once.
The platform writes the change in one region, replication catches up later, and during that window, the child may still be able to play or spend money somewhere else. There is also rarely verifiable proof of exactly when the parent acted.
I built Custody, a neutral system of record for parental consent and minor spending controls, to close that gap.
With Custody, a parent can:
- Grant or revoke consent
- Set a spending cap
- Apply the decision consistently across regions once it commits
- Verify every decision through a tamper-evident audit trail
Every action becomes a link in a hash chain that can be verified directly in the browser.
All data in the demonstration is synthetic. It contains no real minors, biometrics, or personal information.
Why I chose Amazon Aurora DSQL
Custody depends on one critical property:
When a write commits, every region must agree on the result.
Amazon Aurora DSQL is an active-active, multi-region database with strong consistency on commit. It provides the property Custody needs without requiring me to build and maintain a distributed consensus layer myself.
The deployment uses:
-
us-east-1as one regional endpoint -
us-east-2as the second regional endpoint -
us-west-2as the quorum witness
The witness does not expose a database endpoint. Its purpose is to help maintain agreement between the two writable regions.
One framing rule I followed throughout the project was never calling the process “instant.”
A commit requires roughly two cross-region network round trips, and an optimistic-concurrency retry may add more time. The accurate claim is that the system is strongly consistent on commit, without a replication-lag window after the commit completes.
Precision matters more than a superlative.
The data model
Custody uses append-only event tables combined with per-entity projection rows.
Aurora DSQL is PostgreSQL-compatible, but it is not traditional PostgreSQL. The application architecture must respect its specific rules.
A few important examples:
- Primary keys use random UUIDs instead of
SERIAL, because sequences can create hot keys. - DSQL does not support foreign keys, triggers, or stored procedures, so referential integrity is handled in the application layer.
- Indexes are created with
CREATE INDEX ASYNC. - A unique asynchronous index does not enforce uniqueness until its build job finishes, so migrations wait for completion through
sys.jobs.
Each consent action and spending action is stored as an append-only event.
The current consent status and running spending total are stored as projection rows. Each projection is updated in the same transaction as its related event append.
These projections exist per user or per minor. Custody never relies on a single global counter row because that row would become a hot key and create cascading conflicts under load.
Preventing the hash chain from forking
The part of the data model I am most proud of is how it handles concurrent writes.
Each event table uses a composite primary key:
(user_id, seq)
Imagine two requests attempting to append an event for the same user at the same time.
Both requests:
- Read the same current chain tip.
- Calculate the same next sequence number.
- Attempt to insert the next event.
- Collide on the composite primary key.
Aurora DSQL then raises a serialization conflict at commit using SQLSTATE 40001 with error code OC000.
One transaction succeeds. The other retries, reads the new chain tip, and appends after it.
This prevents the chain from forking.
I tested this with eight concurrent append requests. The stored events returned with sequence numbers 1 through 8, with:
- No gaps
- No duplicate sequence numbers
- Every
prev_hashmatching the previous event’sentry_hash
The optimistic-concurrency wrapper
Every write passes through the same retry helper.
The helper:
- Retries only serialization conflicts using
40001 - Uses exponential backoff with full jitter
- Limits the maximum number of attempts
- Retries the entire transaction, never only part of it
- Does not retry normal reads, because DSQL does not conflict-check plain reads
Writes are also idempotent.
The client provides a request UUID, which is stored under a unique asynchronous index. The application uses:
INSERT ... ON CONFLICT DO NOTHING
If the same request is replayed, it becomes a no-op rather than creating a duplicate event.
The tamper-evident audit trail
Custody’s audit trail is a separate SHA-256 hash chain for each user.
Each entry_hash is calculated from:
- The canonical JSON representation of the event payload
- The previous event’s hash
The first event uses a genesis previous hash containing 64 zeros.
Conceptually:
entry_hash = SHA256(canonical_payload + previous_hash)
Canonical JSON is important. Custody follows RFC 8785, which provides sorted object keys and stable number formatting.
Without canonicalization, two logically identical JSON objects could produce different hashes because of differences in key ordering or number formatting. That would create false tampering warnings.
The verification function recalculates every event hash and reports the exact first index where the stored chain no longer matches.
Verification runs in the browser using the Web Crypto API. A judge can modify an event block in the interface and immediately see the chain turn red starting from the first broken link.
Credential-free authentication on Vercel
There is no database password stored anywhere in Custody.
The frontend uses Next.js 16 and runs on Vercel.
At runtime:
- Vercel issues an OpenID Connect token.
- The application exchanges it through AWS
AssumeRoleWithWebIdentity. - AWS grants access to a restricted IAM role.
- The Aurora DSQL connector creates a short-lived, region-scoped authentication token.
A token created for one region only authenticates against that region’s endpoint. Because of this, the application maintains a separate connection pool for each regional endpoint.
This architecture removes the need for permanent database credentials in Vercel environment variables.
Demonstrating cross-region consistency with SSE
The cross-region demonstration uses Server-Sent Events.
Vercel does not host persistent WebSocket servers, so each region has its own SSE route that streams the latest committed state.
When a parent revokes consent:
- A Next.js Server Action submits the transaction.
- The transaction commits to Aurora DSQL.
- Both regional streams read the new committed state.
- Both region panels update.
The us-east-2 panel reads from the second Aurora DSQL endpoint. It is not a duplicated client-side animation.
What appears in the interface reflects the state returned by the two regional database endpoints.
Hardening the public demonstration
The deployed application is open for judges and other users to test, so the public database role follows the principle of least privilege.
It can:
SELECTINSERTUPDATE
It cannot:
DELETE- Drop tables
- Run DDL
- Take ownership of tables
Aurora DSQL also does not support TRUNCATE.
This gives the public application exactly what it needs: read access, append access, and permission to update projection rows. It does not have permission to wipe the database.
Mutation routes are also rate-limited with Upstash Redis, and the event ledger is append-only by construction.
What is wired into the live app
The deployed version includes:
- A multi-region Aurora DSQL ledger
- Strongly consistent cross-region commits
- Optimistic-concurrency retries
- Idempotent writes
- A per-user SHA-256 hash chain
- Client-side tamper verification
- Credential-free Vercel OIDC authentication
- Region-scoped Aurora DSQL tokens
- Live cross-region state streaming with SSE
- SD-JWT age-bracket selective disclosure
- A least-privilege public database role
The age proof uses selective disclosure under RFC 9901. It is not a complete zero-knowledge proof, and I do not claim that it is.
Testing
Custody currently has 88 tests:
- 83 unit tests
- 5 tests that run against the live Aurora DSQL cluster
The live-cluster tests include:
- Cross-region read-after-commit verification
- Eight-way concurrent append testing
- Hash-chain integrity checks
- Idempotency verification
- Optimistic-concurrency retry behavior
Technology stack
- Database: Amazon Aurora DSQL, multi-region and active-active
- Frontend and server: Next.js 16 and React 19
- Hosting: Vercel
- Authentication: Vercel OIDC and AWS IAM
- Rate limiting: Upstash Redis
- Audit trail: SHA-256 hash chains
- Selective disclosure: SD-JWT
- Live regional updates: Server-Sent Events
Try Custody
Custody demonstrates how a globally distributed application can apply parental consent and spending-control decisions consistently across regions while maintaining a verifiable audit trail.
🔗 Live app: https://custody-zeta.vercel.app
Built with Amazon Aurora DSQL and Vercel for the H0 Hackathon.
#H0Hackathon
Top comments (0)