InformationBoard is a free-beta service for creating store, event, and meeting guides and sharing them through a stable link or QR code.
- Node.js 20.9 or newer; Node.js 24 is used in CI.
- npm.
- Copy
.env.exampleto.env.local. - Run
npm ci. - Run
npm run dev. - Open http://localhost:3000.
The application requires four environment variables:
| Variable | Exposure | Purpose |
|---|---|---|
NEXT_PUBLIC_APP_URL |
Browser-safe | Canonical application origin |
NEXT_PUBLIC_SUPABASE_URL |
Browser-safe | Hosted Supabase project URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Browser-safe | sb_publishable_... client key |
SUPABASE_SECRET_KEY |
Server only | sb_secret_... administrative key |
Never expose SUPABASE_SECRET_KEY to Client Components, browser clients, logs,
or committed files.
This repository uses the existing hosted Supabase Free project. It does not use
a local Supabase stack. Schema history is forward-only under
supabase/migrations/.
Authenticate and link the repository:
npx supabase login
npx supabase link --project-ref <project-ref>Preview every remote schema change before applying it:
npx supabase db push --linked --dry-run
npx supabase db push --linked
npx supabase migration list --linked
npx supabase db lint --linked --level error --fail-on error
npx supabase test db --linked supabase/tests/phase2_rls.test.sqlSupabase CLI 2.110 may start a Docker-based pg_prove runner even with
--linked. On the Docker-free development workstation, run the same
transactional test through the Management API and remove the temporary test
extension afterward:
npx supabase db query --linked \
"create extension if not exists pgtap with schema extensions"
npx supabase db query --linked \
--file supabase/tests/phase2_rls.test.sql --output-format json
npx supabase db query --linked \
--file supabase/tests/phase2_rls.test.sql --output-format json
npx supabase db query --linked "drop extension pgtap"Both test runs must finish at assertion 15. The second run proves fixture rollback, and the final command prevents test-only schema drift.
Never run supabase db reset --linked. Remote fixture tests must run inside a
transaction that rolls back, and must stop before real beta customer data is
admitted.
Apply all pending migrations before deploying the application. In particular,
20260729000100_board_images.sql creates the private board-images bucket,
atomic account quota accounting, and the reservation/finalization/cancellation
RPC boundary. 20260730000100_safe_image_board_deletion.sql adds the claimed
image and board deletion boundary. Deploying application code before these
migrations are applied leaves the image workflow unavailable.
The image migration is deliberately non-destructive. Before applying
20260729000100_board_images.sql to a database that already has attachment
rows, inventory unsupported MIME metadata and accounts above the new 50 MiB
limit:
select id, owner_id, storage_path, mime_type
from public.attachments
where mime_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/gif');
with attachment_usage as (
select owner_id, sum(size_bytes)::bigint as attachment_bytes
from public.attachments
group by owner_id
)
select
profiles.id,
profiles.storage_bytes as recorded_bytes,
coalesce(attachment_usage.attachment_bytes, 0) as attachment_bytes
from public.profiles
left join attachment_usage on attachment_usage.owner_id = profiles.id
where profiles.storage_bytes > 52428800
or coalesce(attachment_usage.attachment_bytes, 0) > 52428800;Resolve every returned row and its corresponding private Storage object before
retrying the migration. The migration aborts with
board_image_migration_unsupported_attachments or
board_image_migration_account_over_quota, including an actionable detail and
hint; it never silently deletes legacy metadata. A successful run canonicalizes
display filenames and backfills profiles.storage_bytes.
In Supabase Dashboard, configure:
- Site URL: the production application origin
- Redirect allowlist:
http://localhost:3000/auth/callbackhttp://localhost:3000/auth/confirm<production-origin>/auth/callback<production-origin>/auth/confirm
- Email signup and returning-user login both call
signInWithOtpand use Supabase's default Magic link or OTP template. Keep its{{ .ConfirmationURL }}link unchanged. Both flows redirect through the exact allowlisted<application-origin>/auth/callback, where the PKCE authorization code is exchanged for a server-side session. /auth/confirmremains available for direct token-hash verification in live tests or an optional custom email template. Custom SMTP/template configuration is not required for the default signup and login flow.
Enable the Google provider in Supabase Authentication, save its Google Client ID and Client Secret there, then register the hosted Supabase callback URL shown by that provider screen in Google Auth Platform. Provider secrets must never be copied into this repository.
The production service is deployed on Vercel at https://informationboard-six.vercel.app. Configure these variables for the Production environment in Vercel before deploying:
NEXT_PUBLIC_APP_URLNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYSUPABASE_SECRET_KEYas a sensitive value
Deploy the linked project with npx vercel --prod. The .vercelignore file
keeps all local environment files and Playwright artifacts out of deployment
uploads.
After configuration, manually verify both approved flows:
- Request a magic link for a controlled test address and confirm that it opens
/dashboard. - Sign out and confirm that dashboard access is removed.
- Complete Google consent and confirm that it opens
/dashboard. - Confirm that each new Auth user has a matching
public.profilesrow.
Authenticated owners can create a private board from the store, event, or meeting template, edit it with autosave and safe Markdown preview, recover a newer local draft, and permanently delete it after explicit confirmation. Supabase is the source of truth; per-board JSON import and export are not part of the product.
The authenticated owner E2E scenario accepts a Playwright storage-state file:
E2E_OWNER_STORAGE_STATE=/absolute/path/to/owner-storage-state.json \
npm run test:e2e -- tests/e2e/board-owner.spec.tsOwners can upload JPEG, PNG, WebP, and GIF images. Each stored image is limited
to 10 MiB (10,485,760 bytes), each board can retain at most 20 active image
rows, and all boards for one account share a 50 MiB allowance.
profiles.storage_bytes includes a reservation immediately and continues to
include reserved, transiently cancelling, ready, and transiently
deleting rows until trusted metadata deletion releases the quota.
Display filenames are stripped to a basename, have control characters removed,
are normalized to Unicode NFC, and are limited to 180 code points by both the
action and a database constraint. Filenames never determine Storage paths.
Image bytes stay in the private board-images bucket. Markdown stores stable
/b/[slug]/images/[attachment-id] URLs, so image delivery inherits the parent
board's access mode: public images are anonymous, password-board images require
the scoped access cookie, and private/draft images are available only to their
owner.
Finalization and ready-image deletion claims are service-role-only RPCs. Their server actions first authenticate the owner, load the exact owned board and attachment, verify bytes or saved Markdown as appropriate, and then pass the explicit owner, board, and attachment IDs through the server-only client. Storage INSERT and image-cancellation claims both lock the board before reading or locking the attachment, so either ordering serializes without a deadlock.
Run the focused local database and live browser checks with:
npx supabase test db supabase/tests/phase2_rls.test.sql supabase/tests/phase5_board_images.test.sql
npx supabase test db supabase/tests/phase5_board_deletion_concurrency.test.sql
npx supabase test db supabase/tests/phase5_image_cancellation_concurrency.test.sql
E2E_LIVE_SUPABASE=1 \
npm run test:e2e -- tests/e2e/board-images.spec.tsThe live Playwright setup creates and removes a temporary authenticated owner.
Alternatively, set E2E_OWNER_STORAGE_STATE to an authenticated Playwright
storage-state file as described above. Supplying that path also requires the
real Supabase environment variables; Playwright will not run an authenticated
storage state against the fake browser-test configuration.
Supabase CLI 2.110 currently generates
finalize_board_image.reservation_expires_at as string even though a
successfully finalized SQL row returns NULL. Do not hand-edit the generated
database declaration: the server action validates that ready-state field as
null at runtime, and the generated type should be refreshed when the CLI can
represent the nullable table-return contract.
Owners can publish a board publicly, protect it with a visitor password, or withdraw it to a private draft without changing its slug. Public reads use an anonymous, presentation-only Supabase client. Password content is fetched only through service-role RPCs, with Argon2id verification on the Node.js server.
Password failures are keyed by a domain-separated HMAC of a coarse visitor network key; raw IP addresses are never stored. Five failures within 15 minutes create a 15-minute lock. Successful access uses a versioned, board-scoped, HttpOnly, SameSite=Lax cookie for 12 hours. Replacing or removing the board password invalidates existing access cookies.
PNG and SVG QR files are generated on demand and contain only
NEXT_PUBLIC_APP_URL + /b/[slug]. They never contain a password or access
token, and no QR artifact is persisted.
Run the authenticated Phase 4 browser scenarios against the linked hosted Supabase project with an automatically created temporary owner:
E2E_LIVE_SUPABASE=1 \
npm run test:e2e -- tests/e2e/publishing.spec.tsThe global setup creates a unique magic-link owner and writes its browser
session under the gitignored .playwright/ directory. Global teardown deletes
the Auth user, its cascaded board data, and the local session artifacts even
when a test fails.
An existing owner storage-state file can still be supplied explicitly:
E2E_OWNER_STORAGE_STATE=/absolute/path/to/owner-storage-state.json \
npm run test:e2e -- tests/e2e/publishing.spec.tsnpm run verifynpm run test:e2enpm audit --audit-level=highnpx supabase migration list --linkednpx supabase db lint --linked --level error --fail-on errornpx supabase test db --linked supabase/tests/phase2_rls.test.sqlnpx supabase test db supabase/tests/phase2_rls.test.sql supabase/tests/phase4_publishing.test.sql supabase/tests/phase4_password_access.test.sql supabase/tests/phase5_board_images.test.sqlnpx supabase test db supabase/tests/phase5_board_deletion_concurrency.test.sqlnpx supabase test db supabase/tests/phase5_image_cancellation_concurrency.test.sql
On the Docker-free workstation, run all transactional database suites twice:
npx supabase db query --linked --file supabase/tests/phase2_rls.test.sql
npx supabase db query --linked --file supabase/tests/phase2_rls.test.sql
npx supabase db query --linked --file supabase/tests/phase4_publishing.test.sql
npx supabase db query --linked --file supabase/tests/phase4_publishing.test.sql
npx supabase db query --linked --file supabase/tests/phase4_password_access.test.sql
npx supabase db query --linked --file supabase/tests/phase4_password_access.test.sqlExpected final assertions are 15, 20, and 18 respectively. The repeated runs prove every fixture transaction rolls back cleanly.
The local main database command additionally expects 122 Phase 5 assertions (175 assertions across its four files). The board-deletion and image-cancellation concurrency suites run separately and expect 18 and 17 assertions respectively because they coordinate multiple live database sessions.
The archived 2019 prototype lives under legacy/ and must not be deployed.