Security Model
ARK products are designed to be safe by default, even after you customize them. This page explains how the security model works, what protects what, and which parts you should never change.
The threat model in plain language
ARK products are deployed by buyers, on infrastructure buyers control, holding data buyers own. The threats we design against are:
- A logged-in user reading or writing data they shouldn't.
- An attacker on the public internet hitting your API.
- A leaked secret turning into a data breach.
- A leaked copy of your code letting attackers find vulnerabilities in similar deployments.
We don't design against the buyer themselves — you can do anything you want with your own data and your own infrastructure. We do design so that a casual customization can't accidentally break security.
Row Level Security (RLS) is the foundation
Every product table has Row Level Security enabled in Supabase. Policies live in the database and run on every query, regardless of which client made the request.
What this means in practice
When the React frontend asks Supabase, "give me all the contacts in
track_contacts," the query goes to Postgres with the user's JWT.
Postgres reads the JWT, identifies the user, and applies the RLS
policy:
create policy "Users see contacts in their teams"
on track_contacts for select
using (
team_id in (
select team_id from team_members where profile_id = auth.uid()
)
);If the user isn't in the team that owns the contact, the row is silently filtered out. The frontend never sees it. There's no way to "forget to check" — the database checks for you.
Why this matters for customization
Buyers customize the UI all the time — adding fields, hiding columns, building dashboards. RLS means that even if you accidentally remove a frontend permission check, the data is still protected. The query just won't return rows the user can't see.
The corollary: don't disable RLS on a table unless you understand exactly what you're doing.
Key handling
ARK products use three Supabase keys. Two are public, one is private.
| Key | Where it lives | Safe to expose? |
|---|---|---|
| Project URL | VITE_SUPABASE_URL | Yes — it's just an address |
| Anon public key | VITE_SUPABASE_ANON_KEY | Yes — RLS protects the data |
| Service role key | Not used by ARK products | No — never expose |
The service role key bypasses RLS and can read or write anything in your database. ARK products don't use it because they don't need to — every product operation is appropriate to do as the logged-in user. If you build a custom integration that does need the service role key:
- Store it in a server-side environment variable only.
- Never reference it in any file under
src/(which becomes client-side code in the build). - Use it from a server-side context (a Vercel serverless function, a separate backend, etc.).
If you suspect a service role key has leaked, rotate it immediately: Supabase → Project Settings → API → Service Role Key → Generate New Key.
License key validation
Each ARK product validates its license key on startup against
https://arkteams.io/api/license/validate. The validation:
- Confirms the key exists and isn't revoked.
- Binds the key to the production domain on first valid request.
- On subsequent requests, confirms the requesting domain matches.
The validation endpoint is rate-limited (60 requests per minute per IP). Heavy bot traffic to your deployment can briefly trigger the rate limit, in which case the product allows access (graceful failure) — legitimate buyers are never locked out by infrastructure problems.
What this protects against
- Someone copying the source code and trying to deploy it on a different domain — validation fails.
- Someone copying the license key to a different deployment — validation fails (wrong domain).
What it doesn't protect against
- A buyer hosting the code without an internet connection — the validation network call fails open. This is intentional. We don't lock you out for a network blip.
- A buyer modifying the source to remove validation — possible. We catch this through fingerprinting (next section).
Fingerprinting
Every fingerprinted ARK repo includes around eight invisible markers that let us trace a leaked copy back to the original buyer:
- Build hash comments in 4 file headers
(e.g.,
// Build: ark-a1b2c3insrc/lib/license.js) - CSS micro-variations:
--ark-shadow-popoveropacity in the range0.340–0.360--ark-transition-colorin the range146ms–154ms--ark-radius-mdin the range11px–13px
- One SVG path coordinate shifted by ±0.01
These markers:
- Are deterministic from your buyer ID — re-deriving them produces the same values.
- Are visually imperceptible.
- Survive normal customization. A buyer who edits a feature usually doesn't touch shadow opacity or transition timing.
- Survive partial stripping. Even if a malicious buyer removes the obvious build-hash comments, the CSS micro-variations remain and can identify them.
The Terms of Service explicitly disclose fingerprinting and prohibit tampering with the markers. Honest buyers see no impact; dishonest ones leave a trail.
For the architectural reasoning, see the master planning document section 8 (internal reference).
Password policy
Supabase Auth enforces a configurable password policy. Defaults shipped with ARK products:
- Minimum 8 characters
- Must contain at least one letter and one number
- Common compromised-password check enabled
You can tighten this in Supabase → Authentication → Policies → Password Strength. We recommend turning on HaveIBeenPwned check for production deployments.
For 2FA, see Authentication Model.
Session security
- Sessions live in HTTP-only cookies, which JavaScript can't read. This blocks the most common XSS-based session theft.
- The session cookie has
SameSite=Lax, which prevents most CSRF attacks. - Sessions are JWTs signed by Supabase. Tampering invalidates them.
- Logout revokes the session server-side; the cookie alone isn't enough to log back in.
Public API exposure
ARK products may expose a small number of public endpoints (e.g., a public form, a webhook receiver). Conventions:
- Public endpoints are rate-limited per IP via
lib/rate-limit.ts(or equivalent). - Public endpoints validate every input field.
- Public endpoints never use the service role key.
- Webhooks verify their signature before any side effect (Stripe, GitHub, etc.).
If you add a public endpoint, follow the same pattern. The codebase
includes examples in app/api/license/validate/route.ts (rate
limit) and app/api/webhooks/stripe/route.ts (signature
verification).
What buyers should never change
Some parts of the security model are not safe to remove:
| Don't change | Why |
|---|---|
RLS on shared platform tables (profiles, team_members, etc.) | Protects all users from each other |
| Webhook signature verification | Without it, anyone can POST fake events |
| Rate limits on public endpoints | Without them, you're vulnerable to brute force and DoS |
| Fingerprint markers | They protect ARK's IP and your license investment |
| HTTP-only flag on session cookies | XSS becomes session theft |
| The license validation network call's graceful-failure path | Removing it locks legitimate buyers out on network blips |
Other parts you can change freely:
- Add more RLS policies (don't remove the existing ones).
- Add more rate limits.
- Tighten password policy.
- Enable 2FA.
- Add CSP headers, HSTS, etc.
Reporting a security issue
If you find a security bug in an ARK product, email security@arkteams.io. Don't open a public GitHub issue — that makes it visible to everyone with the same code.
We aim to acknowledge security reports within one business day and ship a fix as a priority update PR to all eligible buyers.
Security checklist before going to production
Run through this when you first launch your deployment:
- Supabase Site URL is set to your production domain
- Supabase email signups are disabled (invite-only)
- Service role key is not referenced in any frontend file
- License key validates successfully on first load
- Custom domain has HTTPS (Vercel handles this automatically)
- All admin users have 2FA enabled
- You've taken a baseline backup in Supabase
- You've documented your environment variables in your password manager so you can recover if Vercel access is lost