Security & isolation

Your data. Physically isolated. Enforced by the database, not the app.

Most SaaS products keep customer data separate with app-layer logic. If the app has a bug, the isolation breaks. Crewnyx enforces isolation at the PostgreSQL layer using Row-Level Security. A bug in our app can't leak data across companies — the database itself refuses.

1. Tenant isolation: how it actually works

Every row in every table has an org_id column. Every read and every write goes through a PostgreSQL Row-Level Security (RLS) policy that says: "You can only see rows where org_id matches the org_id in your session's JWT."

That JWT is signed by Supabase Auth. A user cannot forge it — the signature check happens on every query, in the database, before any row is returned.

-- What every table has
CREATE TABLE time_entries (
  id           uuid PRIMARY KEY,
  org_id       uuid NOT NULL REFERENCES orgs(id),
  user_id      uuid NOT NULL REFERENCES users(id),
  jobsite_id   uuid REFERENCES jobsites(id),
  started_at   timestamptz NOT NULL,
  ended_at     timestamptz,
  ...
);

-- What every table gets
ALTER TABLE time_entries ENABLE ROW LEVEL SECURITY;

CREATE POLICY "isolate_by_org" ON time_entries
  FOR ALL
  USING (org_id = jwt_org_id())
  WITH CHECK (org_id = jwt_org_id());

If Company A's admin runs SELECT * FROM time_entries, Postgres itself returns only Company A's rows. There is no code path — no API endpoint, no SQL injection, no admin tool — that returns Company B's rows to Company A's session. The isolation is not "hidden by the UI." It is enforced by the storage engine.

In plain English Your data lives in the same physical database as other customers' data — but it's stamped with a company ID, and every query is filtered by that ID before the database engine returns anything. Even if we wrote a buggy query that tried to return all rows, Postgres would silently strip out the ones that don't belong to your session's company.

2. Canary tests: proof it works every single deploy

An automated test runs on every code push. It:

  1. Creates two test companies with different data
  2. Logs in as an admin of Company A
  3. Attempts to query Company B's data through every API endpoint
  4. Fails the deploy if any query returns Company B data

If a developer breaks isolation while adding a feature, the code never reaches production. It's not a "we should test that" — it's a hard gate.


3. Payments: we never touch your card

Signup redirects to Stripe Checkout, a Stripe-hosted payment page. Card numbers, expiration dates, CVCs — none of that ever touches our servers. Not in memory, not in logs, not in the database, not in transit through our code.

What we receive from Stripe:

  • An opaque customer_id (like cus_QxyzABC...)
  • An opaque subscription_id
  • The last 4 digits of the card, for you to see in your billing dashboard
  • The card brand (Visa / Mastercard / etc.)

This means we're PCI-DSS compliant by design — we don't need to be certified because we deliberately never handle regulated data. Stripe handles it, and they are certified at Level 1 (the highest).

All webhook events from Stripe are cryptographically signed. If someone tries to send us a fake "subscription paid" webhook, the signature check fails and it's rejected.


4. Authentication & sessions

  • Passwords hashed with bcrypt (cost factor 10+). Never stored in plaintext, never in logs, never sent back.
  • Email verification required before an account activates. No verification = no trial provisioned.
  • Short-lived JWT access tokens (1 hour) + long-lived refresh tokens.
  • Tokens stored in httpOnly, Secure, SameSite=Strict cookies — not localStorage. XSS attacks cannot steal them.
  • Rate limiting: 5 signup attempts per hour per email, 10 login attempts per hour per IP.
  • MFA support for super admin accounts (TOTP via any authenticator app; WebAuthn / hardware keys supported).
  • Session revocation: revoke any active session from the admin dashboard.

5. Transport & storage

  • TLS 1.3 everywhere. HTTP is rejected, not redirected — no downgrade attacks.
  • HSTS preloaded. Browsers refuse to connect to Crewnyx over HTTP even on a first visit.
  • AES-256 at rest for the database and file storage.
  • Cloudflare WAF in front of all public endpoints. Blocks SQL injection, XSS, and known bot patterns.
  • Content Security Policy headers. No inline scripts, restricted resource origins.
  • No third-party trackers on the marketing site or the app.

6. Audit log & forensics

Every tenant-affecting action writes to an audit_log table:

  • Who did it (actor_id)
  • What company it happened in (org_id)
  • What they did (action)
  • When (timestamp)
  • From what IP (ip_address)
  • What the state was before and after (diff)

Super admins can view their company's full audit log at any time. This is your forensic trail if you ever need it.


7. Backups & recovery

  • Point-in-time recovery to any second in the last 7 days
  • Nightly encrypted snapshots retained for 30 days
  • Weekly encrypted snapshots retained for 12 months
  • Backups stored in a separate cloud region from the primary database
  • Documented restore-tested quarterly

8. Compliance posture

  • PCI-DSS: compliant by design (Stripe handles all card data)
  • GDPR / CCPA: data export & account deletion available in the admin dashboard
  • SOC 2: not certified yet. Our underlying infrastructure (Supabase, Stripe, Cloudflare) all hold SOC 2 Type II certifications.
  • Data residency: US primary. EU / regional data residency available on the Enterprise tier.

9. Reporting a vulnerability

If you find a security issue, please email [email protected]. We respond within 1 business day. We do not currently run a bug bounty, but we credit reporters in the changelog with permission.

Last updated: July 11, 2026 · Owned by Sparky Venture Labs LLC

Questions we didn't answer?

If your company has specific compliance or security requirements, talk to us before you sign up. We're happy to walk through the specifics.