Classify your data
Data classification sorts every field your product stores or sends into a class, and the class decides whether it may leave the customer's environment. In a control plane ⇄ data plane design, it decides what each plane holds.
Draw the line per field, not per service: moving whole services is expensive, and one column can decide a security review. You fill the worksheet from your code; the buyer's security review has the final say.
Say "customer data" or "content", not "PII". In a database product, the rows are the content.
Classes
Most fields fall into a class with an obvious answer:
| Class | What it is | Typical fields | Where it goes |
|---|---|---|---|
| Content | What the product exists to store or process | Message bodies, documents, files, code, records, prompts, attachments | Data plane |
| Outputs and derived data | Anything computed from content | Model answers, summaries, reports, search indexes, embeddings, caches, extracted text | Data plane |
| Text that describes content | Short text that reveals content | Subjects, titles, previews, filenames, URLs, query text, custom instructions, error messages with excerpts | Data plane by default |
| Identifiers of outside people | People who aren't users of your product | Email addresses, names, phone numbers, IP addresses | Judgment call |
| Credentials | Access to the customer's systems | OAuth tokens, database passwords, API keys, signed URLs | Data plane, or the customer's own secret store |
| Activity and metadata | Running, securing, and billing the product | Organizations, users, roles, plans, random IDs, timestamps, status, counts, sizes, durations, versions, error categories | Control plane |
Derived data is content. A search index or an embedding can rebuild much of what it was made from.
Judgment calls
Some fields have no default answer. Decide them with the buyer, and write the answer down:
| Field | Why it's hard | Usual answer |
|---|---|---|
| Email addresses, participant names | Needed for routing and limits, but often personal data | Ask. Either answer works once it's written down. |
| Filenames, document titles, repository names | They look like labels but describe content | Data plane |
| Query text | Can reveal what the customer is working on | Data plane when it touches customer data |
| Hashes of sensitive values | A hash of a short or predictable value can be reversed by guessing | Equality lookups only, and only if the buyer agrees |
| Aggregates | Counts over small groups can identify people | Control plane; ask when groups are small |
| Schedules and activity timing | They show how and when the customer works | Control plane, stated in the security review |
| Logs and error reports | Free text can carry content | Send categories and IDs; keep messages in the data plane |
| Training and evaluation data | Needed to improve the product | Compute metrics in the data plane and send numbers |
Tests for each field
Check every field you keep in your cloud for breach, reconstruction, and need:
- Breach. If this table leaked, would the customer's security team call it a breach of their data?
- Reconstruction. Could someone recover or guess the content from it?
- Need. Do sign-in, billing, authorization, scheduling, or operating the deployment need it? If not, don't send it, even when it's allowed.
Then trace every path that crosses between the planes: API responses, events, webhooks, logs, error reports, analytics, and support tools. See Paths that leak.
Worked example
A contract review product stores a customer's contracts, extracts clauses, and flags risky terms with a model. The buyer says "our contracts can't leave our account":
| Field or path | Class | Plane | Crosses as |
|---|---|---|---|
contracts.file, extracted text | Content | Data plane | Never |
contracts.title, counterparty name | Text that describes content | Data plane | Never |
| Clause index, risk flags, summaries | Outputs and derived data | Data plane | Never |
| Reviewer email addresses | Identifier of an outside person | Control plane for now | Metadata report; buyer to confirm |
| Document storage credentials | Credential | Customer's secret store | Never |
contracts.id, uploaded_at, page count, review status | Activity and metadata | Control plane | Metadata report |
| Reviews per month | Activity and metadata | Control plane | Usage report |
review.completed webhook | Path | IDs only; the consumer fetches from the data plane | |
| Model provider error logs | Path | Error category only; provider errors can echo the prompt |
The data plane ends up small: the routes that upload, read, and review contracts, their storage, the clause index, and the model calls. Sign-in, billing, review assignments by ID, and status stay hosted.
Write it down
Keep the result as a table in your design record, one row per field: where it is in the code, its class, its plane, how it crosses, and the reason.
Send the open questions to the buyer. "For this phase" is a valid answer when it's written down, and the next customer will ask the same questions.
Next
- Control plane ⇄ Data plane: the split this worksheet feeds.
- Where the data plane runs: pick the environment for each customer.
- Connect your backend: keep content off your servers.
Whole product
Deploy every service and its storage into the customer's environment, including air-gapped sites, and know what you give up compared to a split.
Connect your backend
How your backend and each deployment talk to each other, and how browsers reach a deployment without customer content passing through your cloud.