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.

×We don't sell your data.
×We don't share access tokens with third parties.
×We don't run analytics on your content.
×We don't store your platform passwords.
×We don't use your data to train AI models.
×We don't serve ads or share data with advertisers.

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

Privacy PolicyTerms of Service