Security
How we protect your data.
Trellis is built on Google Cloud with production security practices from day one. Not because we had to. Because that's how it should be done.
What we access
We only read your field names and a few sample rows during setup. We never store, copy, or modify your content without your explicit permission. During the architect flow, Trellis reads your Airtable table structure and up to 3 sample rows per table to help you configure your CMS. That's it.
Authentication
Trellis uses Firebase Authentication for all user accounts — email/password or Google OAuth. When you connect Airtable or Webflow, you authenticate directly with those platforms via OAuth. We never see or store your Airtable or Webflow passwords. We receive a scoped access token, which we encrypt before storing.
Token encryption
All OAuth tokens are encrypted at rest using AES-256-GCM — the same encryption standard used by banks and government systems. Tokens are encrypted before writing to our database and decrypted only at the moment we make an API call on your behalf. The encryption key is stored in environment variables, never in code or in the database.
Database security
Firestore security rules enforce per-user isolation at the database level. Every document is scoped to the authenticated user. No user can read, write, or even know about another user's data. This isn't a feature we toggle on — it's enforced by the database engine itself.
App Check
Firebase App Check with reCAPTCHA Enterprise validates that requests to our backend come from our actual app — not bots, scripts, or spoofed clients. This prevents automated abuse before it reaches our API.
Rate limiting
Auth routes (login, signup): 20 requests per 15 minutes. Webhook endpoints: 100 requests per minute. Sync trigger endpoints: rate limited to prevent runaway API costs. If something tries to hammer our API, it gets blocked before it can cause damage.
CORS policy
API requests are only accepted from trelliscms.com. No wildcard origins, no exceptions. If a request doesn't come from our domain, it's rejected at the network level.
Content Security Policy
CSP headers restrict which scripts and resources can load on our pages. Only our own code, Firebase services, Stripe.js, and reCAPTCHA are allowed. No third-party scripts can inject themselves into Trellis.
Infrastructure
Trellis runs on Google Cloud Platform via Firebase. That means our infrastructure inherits Google's SOC 2 Type II, ISO 27001, and other compliance certifications. We don't need to build a data center — we run on one of the most secure cloud platforms in the world.
Site transfers
When you transfer a site, the new owner connects their own platform accounts. Your access tokens are never shared. The new owner authenticates directly with each platform and receives their own scoped tokens, encrypted independently. Your credentials are revoked the moment the transfer completes.
Open source
Our actual security rules, published openly.
Most apps ship with test-mode database rules that let anyone read anything. We don't. Below are our actual Firestore security rules — every collection scoped to the authenticated user, default-deny on everything else. We publish them because we have nothing to hide.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Default: deny all
match /{document=**} {
allow read, write: if false;
}
// Users collection: authenticated users can only access their own doc
match /users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.keys().hasAll(['email', 'plan', 'createdAt'])
&& request.resource.data.plan == 'free';
allow update: if request.auth != null
&& request.auth.uid == userId
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['createdAt']);
}
// Connections collection: users can only access their own connections
match /connections/{connectionId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.keys().hasAll(['userId', 'platform', 'encryptedAccessToken', 'createdAt']);
allow delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
// Sync configs: users can only access their own sync configs
match /syncConfigs/{syncId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
// ID mappings: nested under syncConfigs, inherit parent auth
match /idMappings/{syncId}/records/{recordId} {
allow read, write: if request.auth != null;
}
// Sync logs: users can read their own logs
match /syncLogs/{logId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
}
// Wizard drafts: nested under user, inherits user isolation
match /users/{uid}/wizardDrafts/{draftId} {
allow read, write: if request.auth != null
&& request.auth.uid == uid;
}
// Sites: users can only access their own sites
match /sites/{siteId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
// Collection mappings: users can only access their own
match /collectionMappings/{mappingId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
// OAuth states: server-written, short-lived
match /oauthStates/{stateId} {
allow read: if true;
allow write: if false;
}
}
}Commitments
What we don't do.
Infrastructure
Firebase Auth · AES-256-GCM encryption · Firestore per-user isolation · reCAPTCHA Enterprise · SOC 2 Type II compliant infrastructure (Google Cloud)
Questions about security? Email skye@trelliscms.com