Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Local development defaults. Copy to .env.local to get started:
#
# cp .env.example .env.local
#
# These are the standard values printed by `npx supabase start`. The anon key is the published
# local demo key, not a secret -- it is meant to be public and is enforced by row level security.
# Production values live in .env.prod, which is not in the repository.
#
# .env.local is deliberately NOT tracked: the Vercel CLI appends a VERCEL_OIDC_TOKEN to it on
# `vercel link` / `vercel env pull`, and this repository is public.

# Inlined into the client at build time, so the build fails without them.
PUBLIC_SUPABASE_API_URL=http://127.0.0.1:54321
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0

# Used by Supabase auth for redirects. Nothing in src/ reads it.
SITE_URL=http://localhost:3000
3 changes: 0 additions & 3 deletions .env.local

This file was deleted.

26 changes: 6 additions & 20 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,11 @@ on:
pull_request:
workflow_dispatch:

# Don't let a stale run keep reporting on a branch that has already moved on.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: supabase/setup-cli@v1
with:
version: latest

- name: Start Supabase local development setup
run: supabase db start --exclude storage-api,imgproxy,logflare,supavisor,vector

- name: Verify generated types are checked in
run: |
supabase gen types typescript --local > types.gen.ts
if ! git diff --ignore-space-at-eol --exit-code --quiet types.gen.ts; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
uses: ./.github/workflows/test.yaml
83 changes: 77 additions & 6 deletions .github/workflows/production.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
name: Deploy Migrations to Production
name: Deploy to production

on:
push:
branches:
- main
workflow_dispatch:

# Never run two deploys at once, and never cancel one partway: a cancelled run could leave
# migrations applied with the matching app never deployed.
concurrency:
group: production
cancel-in-progress: false

jobs:
deploy:
# The same suite the pull request had to pass.
test:
uses: ./.github/workflows/test.yaml

# Check the Vercel credentials resolve before anything touches the production database. Without
# this, a bad or expired token fails at `vercel pull` in the deploy job -- after migrations have
# already been applied, and `supabase db push` cannot be rolled back.
verify-deploy-credentials:
needs: test
runs-on: ubuntu-latest
environment: production

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

steps:
- run: npm install --global vercel@latest
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

# Migrations go first. The app deploy calls database functions that migrations create, so
# deploying the app first would break it until this catches up.
#
# Because of that ordering the old app runs briefly against the new schema, so migrations need to
# stay backward compatible with the currently deployed app: add in one release, remove in a later
# one. `supabase db push` cannot be rolled back.
migrate:
needs: [test, verify-deploy-credentials]
runs-on: ubuntu-latest
environment: production

Expand All @@ -19,9 +52,47 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: supabase/setup-cli@v1
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

# The CLI from devDependencies, so migrations are applied by the same version the tests ran
# against rather than whatever is newest that day.
- run: npm ci

- run: npx supabase link --project-ref $SUPABASE_PROJECT_ID
- run: npx supabase db push

# Vercel's own Git integration must be turned off for production, or this gate is bypassed and
# the app ships without waiting for either the tests or the migrations.
deploy:
needs: migrate
runs-on: ubuntu-latest
environment: production

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
version: latest
node-version: 22
cache: npm

- run: npm install --global vercel@latest

# `vercel pull` brings down the project's production environment variables, so the PUBLIC_*
# values the build inlines come from Vercel's project settings rather than being duplicated
# into GitHub secrets.
- name: Pull Vercel environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

- run: supabase link --project-ref $SUPABASE_PROJECT_ID
- run: supabase db push
- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
82 changes: 82 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Test

# The whole suite, in one place, called by both the pull request workflow and the deploy workflow so
# the checks that gate a deploy are exactly the checks that ran on the pull request.
on:
workflow_call:

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

# Use the CLI from devDependencies rather than supabase/setup-cli, so CI and developers run
# the exact version pinned in package-lock.json. Type generation output varies between CLI
# releases, and a floating version would fail the drift check below on unrelated changes.
#
# `supabase start`, not `supabase db start`: the e2e harness creates accounts through
# auth.admin.createUser and reads through PostgREST, so those services have to be running.
#
# Everything the tests do not touch is excluded, which skips those image pulls -- worth doing
# because Docker Hub rate limits have failed runs here. The tests need the database, the API
# gateway, PostgREST and auth; nothing exercises realtime, email, storage or edge functions.
# Use the CLI's own service names (`supabase start --help`): unrecognised names, which this
# repo previously had, are silently ignored and exclude nothing.
- name: Start Supabase
run: >
npx supabase start
-x studio,inbucket,imgproxy,storage,analytics,vector,edge-runtime,functions,meta,realtime

# Regenerate into the committed path and fail if it differs. Writing to any other filename
# makes this check silently pass, because git diff ignores untracked files.
- name: Verify generated types are up to date
run: |
npm run types
if ! git diff --ignore-space-at-eol --exit-code -- src/database/database.types.ts; then
echo "::error::src/database/database.types.ts is out of date. Run 'npm run types' and commit the result."
exit 1
fi

# svelte-kit sync generates the $env/static/public module from the environment it can see, so
# .env has to exist before it runs or PUBLIC_SUPABASE_* are missing from the generated types
# and every file importing them fails to check. Values come from the running stack rather than
# .env.example so a port change cannot make CI disagree with reality.
- name: Write .env from the running stack
run: |
eval "$(npx supabase status -o env | sed 's/^/export SUPABASE_/')"
{
echo "PUBLIC_SUPABASE_API_URL=$SUPABASE_API_URL"
echo "PUBLIC_SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY"
} > .env

# sync also generates .svelte-kit/tsconfig.json and the ./$types for each route. A working
# copy usually has them from a previous dev or build run; a fresh checkout does not, and
# svelte-check cannot resolve anything without them.
- name: Check types
run: |
npx svelte-kit sync
npx svelte-check --tsconfig ./tsconfig.json

- name: Unit tests
run: npx vitest run

# The harness defaults to the local stack, and reads SUPABASE_URL / SUPABASE_ANON_KEY /
# SUPABASE_SERVICE_ROLE_KEY when they are set. Pass them explicitly so a change to the
# local defaults can't silently point the suite somewhere unexpected.
- name: End-to-end tests
run: |
eval "$(npx supabase status -o env | sed 's/^/export SUPABASE_/')"
SUPABASE_URL="$SUPABASE_API_URL" npm run test:e2e

# Build last, so a broken build is not discovered during a deploy. It reads .env written above.
- name: Build
run: npm run build
14 changes: 9 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ node_modules
/build
/.svelte-kit
/package
.env
.env.local
.env.prod
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vercel
test-results
test-results

# Every env file is local-only. The Vercel CLI appends a VERCEL_OIDC_TOKEN to .env.local on
# `vercel link` and `vercel env pull`, and this repository is public.
.env*

# ...except the template of local defaults. This negation has to come after the .env* rule above:
# gitignore applies the last matching pattern, so putting it earlier silently has no effect.
!.env.example
42 changes: 42 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,48 @@
We'll document all notable changes in this file, including bug fixes, enhancements, and dependency updates.
Dates should be in`YYYY-MM-DD` format and versions are in [semantic versioning](http://semver.org/) format.

## v0.9.0 2026-07-18

### Security

- Fixed a row level security policy that let anyone, including unauthenticated callers, add
themselves as an administrator of any organization. The policy compared a profile's organization
id to its own primary key, which is never true, so the check it was meant to make always passed.
- Stopped members from promoting themselves to administrator, or changing their own supervisor. The
policy restricted which rows a person could edit but not which columns, and editing your own name
and bio uses the same permission.
- Restricted editing a comment to its author or an administrator, matching what deletion already
required. Previously any member could rewrite anyone else's comment.
- Limited who can see an organization's roles to its members, unless the organization is public.

### Fixed

- Adding a person, or making any other change, no longer waits on a realtime notification to appear.
Realtime subscriptions were being torn down when revisiting an organization, after which nothing
refreshed until the page was reloaded by hand.
- Comments left by a member on something they could not otherwise edit were silently discarded.
- Deleting a comment you lacked permission to delete could still detach it from view.
- Renaming a team recorded the change against the organization instead of the team, so it never
appeared in the team's history.
- Unchecking all subtasks did not wait for the changes to save.
- A paragraph following a block quote was dropped, and each line of a multi-line quote repeated the
first line.
- Adding a subtask that failed to save still moved the cursor into it.

### Added

- An end-to-end test suite covering every database operation, including who is permitted to perform
each one.
- Seed data for local development, so a fresh database has an organization to work with.
- Continuous integration now runs the tests, and deploys wait for them to pass. Database migrations
are applied before the app that depends on them.

## v0.8.11 2026-07-18

### Changed

- Updated minor dependencies; fixed migration to `write-excel-file` 4.0.

## v0.8.10 2026-04-02

### Changed
Expand Down
58 changes: 58 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# End-to-end tests

These tests run the real `Organization` API against a real local Supabase database. They cover
PostgREST, row level security, and the triggers that link profiles to people — none of which a
mocked client would catch. Several of the bugs these were written to guard against (an INSERT
policy that let anonymous callers make themselves admins, profile↔person linking that broke on
email casing) were invisible at the TypeScript level and only observable against a live database.

## Running

```sh
npx supabase start # the tests need the local stack
npm run test:e2e
```

Run one file:

```sh
npx vitest run --config vitest.e2e.config.ts e2e/roles.test.ts
```

The tests connect to `http://127.0.0.1:54321` with the standard local demo keys. Override with
`SUPABASE_URL`, `SUPABASE_ANON_KEY`, and `SUPABASE_SERVICE_ROLE_KEY` if your stack differs. They
never touch a remote database — but note they create and delete real rows, so never point them at
production.

## How a test is built

`harness.ts` provides the shared fixtures:

- `createTestOrg(name)` builds an org through the real `create_org` RPC (burning an invite, the same
path the app uses) and returns three people: an `admin`, a non-admin `member`, and an `outsider`
who belongs to no org. Each carries a `db` — an `Organization` bound to their own authenticated
client — so a test can assert what each role is actually permitted to do.
- `admin` is a service-role client that bypasses RLS. Use it to set up preconditions and to assert
what really landed in the database, never to perform the operation under test.
- `anonClient()` is a signed-out client for testing the anonymous boundary.
- `cleanup` removes every org and auth user the file created; call it from `afterAll`.

Two conventions matter:

1. **Assert database state, not the absence of an error.** A mutation silently filtered out by RLS
returns no error, so `expect(error).toBeNull()` alone proves nothing. Read the row back with the
service-role client and check it.
2. **Every mutating operation gets an access-control test.** Assert that a member, an outsider, or
an anonymous caller cannot perform admin-only operations, and that the database is unchanged
afterward.

Files run serially (`fileParallelism: false`) because they share one database.

## Coverage

`coverage.test.ts` enforces completeness: it parses every method declared on `Organization`,
subtracts the pure helpers that never touch the database, and fails if any remaining method is not
referenced by a test. Adding a new CRUD method without a test breaks the suite and names the method.

If you add a genuinely non-database method, add it to that file's `NON_DATABASE` set — a second
assertion fails if that set names a method that no longer exists, so the exemptions can't rot.
Loading
Loading