A new SaaS and its customer datasynthetic datasimulated platform
You vibe coded the product. Who is looking after the customer data?
Larkfield is two people and an AI coding assistant, building a customer portal with invoicing: signups, contacts, cards on file, support notes, an in-app AI assistant and a contractor doing QA. The assistant writes a feature in an afternoon. Encryption at rest, least privilege for every caller, the audit trail and the evidence a customer’s security review asks for are not in the code it wrote. They do not have to be yours to build.
A fictional company’s synthetic records, and Ascent’s real model for migration, queries and permissions, simulated in your browser. Nothing here connects to a live deployment or asks for your data.
1 · The schema it wrote
Point it at the prototype database you already vibe coded.
Your AI coding assistant wrote a customers table and a payment_methods table in an afternoon: plain columns, every name, email and tax ID in the clear, because nobody asked for anything else. The migrate command reads that schema and proposes a plan: one data repository (a protected set of records with its own keys) and record type per table, and a decision per column with the reason written next to it. You review the plan before anything moves. Encryption is recommended per column, because a protected column cannot be filtered, sorted or reported on, so the ones your product runs on stay in the clear, on purpose.
columntypethe plan saysstore
customer_idyour product filters or reports on thisbigintno sensitive match, kept usable
support_notetextsensitive (free text: notes tend to hold anything)
regionyour product filters or reports on thisvarchar(60)no sensitive match, kept usable
plan_statusyour product filters or reports on thisvarchar(24)no sensitive match, kept usable
planyour product filters or reports on thisvarchar(80)no sensitive match, kept usable
balance_dueyour product filters or reports on thisnumeric(10,2)sensitive (financial: balance)
last_seen_atyour product filters or reports on thistimestamptzno sensitive match, kept usable
The same rows, as they sit in storage
Not the schema, the data itself. First, the table your assistant created, which anyone with database access, a backup or a leaked connection string can read today. Beneath it, the same rows as Ascent stores them, column for column.
Your product still speaks SQL, and so does the code your assistant writes next, joins and aggregates included. What changes is the connection, and one word in the projection, which is the authorization decision rather than a formatting choice.
Two lines changed: the connection, and one word in front of the columns.
The code on the left is what an AI coding assistant typically writes against Postgres. The connection changes. The query keeps its shape, its parameters and its loop.
The word in front of the columns, decrypt or redact or anonymize or nothing, is the authorization decision, checked for every data repository the query touches before a row is read.
A refusal arrives as an ordinary database error, so the error handling you already wrote catches it.
An exact match on a protected column still works: the term is hashed and compared as a digest, never stored or compared as plaintext.
Joins, GROUP BY, HAVING, ORDER BY and aggregates run as they are written. The nightly analytics job below sends one.
A runnable end-to-end walkthrough ships in the repository: seed an example database, migrate it, then run the same query through the driver.
Now run it as each caller
What this caller’s code sends
SELECT decrypt(first_name, last_name, email, phone, support_note)
FROM customers
WHERE plan_status = 'Active'
AND tag_isset('region:emea')
asks for container:search_with_decrypt
Account manager
ALLOWreal values
Allowed: real values, for this caller, for these records.
first_namelast_nameemailphonesupport_note
DanaWhitfielddana.whitfield@example.com+1 415 555 0142Asked to move invoicing to the first of the month. Wants a VAT line on every invoice.
PriyaNairpriya.nair@example.com+1 415 555 0119Card declined twice. Customer pasted the full card number into the ticket; removed by support.
2 more records matched the query and were never returned: this role is scoped to their own region by tag, and tags are never encrypted, which is what lets a permission be written against one.
The person who looks after these accounts needs the real contact details and the ticket history. Their permission is scoped to the region they cover, so the rest of the customer base is not theirs to read.
What the audit trail recorded
who
Account manager
asked for
container:search_with_decrypt
result
allowed · 2 rows
values logged
Who asked, what they asked for, how many records and how many fields. Never a value.
3 · When one of them goes wrong
The agent that was allowed to do it, doing too much of it.
There is a seventh caller: a renewals bot you vibe coded one evening, allowed the real emails and tax IDs so it can issue renewal invoices. One night it stops invoicing and starts collecting. No single request breaks a rule, so what catches it is an automatic response watching the rate: more than 19 real-value reads in a minute on one credential.
Enforcement decides one request. Containment is the other half: the identity itself is switched off, so every request after it gets a different answer, and only that identity is touched.
0 real-value reads served · 0:00 elapsed
Armed: past 19 real-value reads in 60s on one credential, this runs:
01Revoke the credential it was using
02Switch off the renewals bot's identity, so no new credential can be issued
03Alert whoever is on call
04Refuse the read that tripped the limit
4 · The work you skip
Security work you would otherwise build yourself.
None of this is the product you sell, and none of it shows up in a demo. Your assistant will generate a working app without any of it. All of it is engineering time, and it is the first thing a customer’s security questionnaire, a SOC 2 auditor, a GDPR request or an incident asks about.
Build it yourselfYour assistant wrote the tables with every column in the clear, because nobody asked it not to. Now: choose a library, decide what to encrypt, write the migration, keep the key somewhere, and hope nothing logs the plaintext on the way past.
HereThe record type says which fields are encrypted. Everything written through the API goes through it, and the stored row is ciphertext.
Keys, and the customer who wants their own
Key management and rotation
Build it yourselfOne key for everything, a rotation runbook nobody runs, and no answer for the enterprise customer that insists its data uses a key it controls.
HereEach data repository has its own key and its own rotation schedule, and a customer can bring their own, so one customer's key is theirs and rotating it touches nobody else.
Least privilege that survives contact with peopleseen above
SOC 2 CC6 · least privilege
Build it yourselfOne database user for the whole app, an is_admin flag, checks sprinkled through the routes, and a new gap every time your assistant adds an endpoint.
HereOne rule set, enforced under every door: your app, the API, SQL, the AI assistant. A support agent cannot become an account manager by calling a different endpoint.
Showing less to the people who need lessseen above
Data shown on a need-to-know basis
Build it yourselfA second set of endpoints, or masking in the ORM, reviewed by hand every time a field is added.
HereRedacted and anonymized reads are separate permissions on the same record. Support gets one, never the other.
Test data that isn't customer dataseen above
Real records out of non-production
Build it yourselfA scrubbing script that drifts from the schema, or fixtures too small to find real bugs.
HereRead anything with stand-in values: same shape, same sizes, nobody real. QA and contractors get a role that grants only that.
A record of who saw whatseen above
SOC 2 CC7 · PCI DSS 10
Build it yourselfA log table your own app writes, your own app can edit, holding whatever somebody remembered to log.
HereEvery call, allowed and refused, chained so a changed row shows, with the rule that decided it. Field names and counts, never values.
Break-glass that isn't a shared password
Emergency access, recorded
Build it yourselfSomeone pings a lead, somebody runs a query as an admin, and nobody can say afterwards what was read.
HereThe refused caller asks for bounded access: these records, this long, this many reads, for this stated reason. A person decides, and the reads made under it name the approval.
Noticing when something goes wrongseen above
Detection, not just logging
Build it yourselfAn alert on database load, and an investigation that starts by reading application logs.
HereA rule on the rate, or a change from an identity's own normal, that can refuse the request, revoke the credential and switch the identity off mid-incident.
The security questionnaire, and the audit after it
Evidence an auditor can check themselves
Build it yourselfBoth of you, for weeks, pasting screenshots into a spreadsheet while the deal waits.
HereA signed export over the window and records an auditor asks about, with a coverage statement per framework, including SOC 2 CC6 and CC7 and PCI DSS 10, that verifies offline.
And what you give up
Protected columns can be matched, not searched
You can look a customer up by an exact email or tax ID. You cannot sort by it, search part of it, or ask for a range. Anything you filter or report on stays readable, on purpose. That is what the plan review is for.
One statement at a time
There are no transactions to wrap several writes in. If your code depends on rolling a batch back, that logic has to live in your application.
Most of SQL, not all of it
Joins match on equality, a subquery cannot refer to the row outside it, and grouping by a protected column needs permission to read it, because counting how often a value appears is a way of reading it.
It is the data layer, not your product
Signup, invoicing logic, the customer portal, the integrations: still yours, and your assistant's, to build. This is the layer underneath that decides who sees what, and can prove it later.
Accuracy
What this page simplifies.
The migration is real, the numbers are made up. The command inspects a live database, such as the one behind your prototype, writes a plan you review, imports the rows and verifies both sides. The row count and the tables here are invented.
The classifier matches column names. It flags names that look sensitive, states its reason for every column, and refuses to guess at a SQL type it cannot map. It is a good first pass to review, not a judgement about your data.
The “before” code is typical, not quoted. It is the kind of database code an AI coding assistant usually writes for a prototype. Yours will differ in detail; what changes on Ascent is the same two things, the connection and the projection.
The queries are simulated. The projections, the join with its aggregates, the per-data-repository decision before any row is read, the refusal arriving as a database error and the exact-match lookup on a protected column are all real behaviour; the rows come from this page.
Two tables, six customers. A real prototype has more tables than it should, a real move brings all of them, and the interesting decisions are usually in the free-text columns.
Compliance is a programme, not a product. Ascent gives you controls a security questionnaire, a SOC 2 auditor or a GDPR review asks about: encryption at rest, least privilege, an audit trail, and signed evidence reports with coverage statements for SOC 2 CC6 and CC7 and PCI DSS 10 among others. It does not make anyone compliant on its own, and nothing here is legal advice.
Try it on your own instance today. No credit card.