Field Classifications
Grove provides built-in field-level data classification through the
@piiand@confidentialannotations. These annotations drive automatic encryption at storage boundaries, control redaction in API responses, and integrate with the authorization system's classification hooks.
Prerequisites: Declaration Grammar for record and event field syntax, Authorization Hooks for classification hooks. What you'll learn: How to annotate fields, what happens at storage and retrieval boundaries, and how classification interacts with queries and audit logs.
Overview
Field classifications are metadata annotations on record and event fields that declare the sensitivity level of the data they contain. Grove recognizes two classification levels:
| Annotation | Sensitivity | Default Behavior |
|---|---|---|
@pii | Personally Identifiable Information | Encrypted at rest, redacted in responses unless revealed |
@confidential | Business-sensitive data | Encrypted at rest, redacted in responses unless revealed |
Fields without annotations are considered "public" and are stored and returned without special treatment.
Annotating Fields
Annotations appear before the field name in record and event declarations:
record Customer {
name: String
@pii email: String
@pii phone: String?
@pii social_security: String
@confidential credit_limit: Decimal
tier: String
}
event CustomerCreated:1 {
@pii name: String
@pii email: String
tier: String
}
Annotation Rules
- Placement. Annotations must appear immediately before the field name, on the same line or the preceding line.
- Single annotation per field. A field may have at most one classification annotation.
@piiand@confidentialare mutually exclusive. - Inheritance. If a record field references another record type, the nested record's annotations are preserved. Classification does not propagate up or down automatically.
- Collections. Annotating a
ListorMapfield classifies the entire collection, not individual elements.
record Profile {
@pii addresses: List<Address> // the entire list is classified as PII
@confidential metadata: Map<String, Value>
}
Encryption at Storage Boundaries
When a classified field is persisted to a database, the Grove runtime automatically encrypts its value:
Encryption Behavior
| Operation | @pii | @confidential | Unclassified |
|---|---|---|---|
| Write to database | AES-256-GCM encrypted | AES-256-GCM encrypted | Plaintext |
| Read from database | Decrypted in memory | Decrypted in memory | Plaintext |
| Event serialization | Encrypted in event store | Encrypted in event store | Plaintext |
| API response (default) | Redacted | Redacted | Included |
| API response (revealed) | Included (decrypted) | Included (decrypted) | Included |
| Audit log | Redacted | Redacted | Included |
Encryption Key Management
- Encryption keys are managed through the Credential Store.
- Each classification level can use a separate encryption key.
- Key rotation is supported; re-encryption happens lazily on read (decrypt with old key, re-encrypt with new key on next write).
Database Column Handling
Classified fields are stored in database columns with a _enc suffix to distinguish them from plaintext columns:
-- Generated DDL for Customer table
CREATE TABLE customer (
pk TEXT PRIMARY KEY,
id TEXT NOT NULL,
name TEXT NOT NULL,
email_enc BLOB NOT NULL, -- @pii: encrypted
phone_enc BLOB, -- @pii: encrypted, nullable
social_security_enc BLOB NOT NULL, -- @pii: encrypted
credit_limit_enc BLOB NOT NULL, -- @confidential: encrypted
tier TEXT NOT NULL,
version INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
Redaction in API Responses
By default, classified fields are redacted in API responses. The redacted value depends on the field type:
| Field Type | Redacted Value |
|---|---|
String | "[REDACTED]" |
Int, Decimal | 0 |
Bool | false |
Id | "[REDACTED]" |
List<T> | [] |
Map<K,V> | {} |
Optional (T?) | null |
Revealing Classified Fields
The classification hook determines which fields are visible to a given caller:
classification(ctx) {
if ctx.principal.roles.contains("support:full") {
reveal @pii
reveal @confidential
} else if ctx.principal.roles.contains("support:basic") {
reveal @pii
}
// Without explicit reveal, fields remain redacted
}
See Authorization Hooks for full details on the classification hook.
Classified Fields in Queries
When a query accesses a classified field, the classification is preserved through the query result. The field is decrypted in memory during query execution but redacted in the response according to the caller's classification hook.
query get_customer(id: Id) -> Customer {
// email is decrypted for query logic
let customer = customers.get(id)
return customer // email will be redacted in the response unless revealed
}
Filtering on Classified Fields
You may filter on classified fields within queries. The runtime decrypts the field for comparison but does not expose it in the response unless revealed:
query find_by_email(email: String) -> Customer? {
return customers.find(c => c.email == email)
}
Note: Filtering on encrypted fields requires full-table scan since encrypted values cannot be indexed. For high-performance lookups on PII fields, consider maintaining a separate hashed index.
Classified Fields in Audit Logs
Classified fields are always redacted in audit logs, regardless of the caller's classification level. This ensures that sensitive data does not leak into the audit trail.
audit update_customer(ctx, result) {
log {
action: "update_customer",
customer_id: result.id,
// Do NOT log PII fields
updated_fields: result.changed_fields,
principal: ctx.principal.id,
}
}
Best Practices
-
Classify at the source. Apply annotations when defining the record or event, not after the fact. This ensures encryption is applied from the first write.
-
Minimize PII in events. Events are immutable. Once PII is written to the event store (even encrypted), it persists forever. Consider storing a reference (ID) to the PII rather than the PII itself.
-
Use @confidential for business data. Reserve
@piifor data that identifies a natural person. Use@confidentialfor sensitive business data like pricing, financial terms, or internal strategies. -
Test redaction. Write tests that verify classified fields are properly redacted for unauthorized callers:
test "customer email is redacted for unauthenticated requests" { let customer = create_customer(name: "Alice", email: "alice@example.com") let result = get_customer(customer.id) assert result.email == "[REDACTED]" }
See Also
- Authorization Hooks -- classification hooks that control field visibility
- Credential Store -- encryption key management
- Type System -- field type definitions
- Declaration Grammar -- annotation syntax in declarations