Trust

Security & access

No passwords, no OAuth, no signup forms. Authentication is a magic link to your inbox; authorization is a Postgres RLS policy. Here's exactly what that protects, and what it doesn't.

Auth flow

We use Supabase Auth's magic-link flow end-to-end. You enter an email, Supabase emails a one-time link, the link grants a signed JWT. That JWT is the only thing the app ever checks. There is no password anywhere in the codebase — not stored, not hashed, not hinted at.

Auth flow diagram: email → magic link → members lookup → localStorage identity → RLS-protected queries.
The five steps from email to authorized query. localStorage is a convenience cache; the JWT is what actually unlocks data.

Identity

Once authenticated, the app caches your chosen member context in localStorage under the key taskflow-identity:

memberId
UUID of your row in the members table.
name
Your display name. Also stored on the members row.
workspaceId
The workspace you last opened. You can belong to more than one.

localStorage is a cache, not a credential. If a browser clears it, re-authenticating with the same email restores everything — your member row is keyed by email, not by the cached UUID.

Row-level security

Every workspaced table has an RLS policy that compares the row's workspace_id to the workspace the JWT belongs to. Anything that doesn't match returns zero rows — not 403, zero rows. Queries are safe to run even if the client guessed a workspace UUID.

Two users from different workspaces both trying to read the same tables. The RLS policy allows each to read their own, blocks cross-workspace reads.
Ava and Ben never see each other's rows. The wall is the database, not the client.

What we store

Email
Stored on the members row. Used to match authenticated users back to workspaces. Never shown to other members.
Name
Your display name on the members row. Visible to teammates.
Task content
Title, description, acceptance criteria, importance, and status. Visible to every member of the workspace.
Notes & feed
Markdown notes per project and activity feed entries. Workspace-scoped.
Images
Task attachments in the task-images storage bucket, path-scoped by task ID.
Stats
XP, streaks, and completion counts in member_stats. Visible as a leaderboard.

What we don't store

  • No passwords. Ever. There is no password column anywhere in the schema.
  • No location, IP history, or device fingerprints beyond what the hosting provider logs.
  • No third-party analytics SDKs, no ad pixels, no session replay.
  • No emails from teammates in your browser — only your own, via the Supabase session.

Threat model · what RLS protects

Guarantee
Cross-workspace reads. A member of workspace A cannot read workspace B's tasks, notes, members, or stats — even with a crafted query or a known UUID. The JWT simply doesn't match.
Guarantee
Unauthenticated reads. Without a valid Supabase session, every query returns zero rows. You can't probe the API for workspace existence.
Guarantee
Client tampering. Forging a new memberId in localStorage does not grant access — the server checks the JWT, not the cache.

Threat model · what RLS does not

!
No per-object ACLs
Any authenticated member of a workspace can read every task, note, and stat in that workspace. There is no way to hide a task from a specific teammate. This is by design — Taskflow is built for trusted teams of two to five, not for compartmentalized access control.
!
Invitation = email insert
To add a teammate, an existing member inserts their email into the members table. That email, when authenticated, gains full workspace access. There is no separate share link or join token to revoke — to remove access, delete the member row.
!
Magic-link inbox security
A magic-link auth model is only as secure as the recipient's email account. If someone else reads your inbox, they can click the link. Links are single-use and short-lived, but if you receive one you didn't request, assume your email is compromised.
!
localStorage is clearable
Browser privacy features and incognito sessions will clear the cached identity. You'll need to re-authenticate via magic link, but no data is lost — the members row persists.

Reporting issues

If you find something that looks like it bypasses the isolation model above, please don't post it publicly. Open a private security advisory on the repository or email the maintainer. Fixes go out as soon as we can reproduce.

That's the tour

You've seen the flows, the architecture, the gamification layer, and the security model. The fastest way to get the rest is to click around the demo.

Open the demo workspace →