Quickly browse a collection of links or files with one short URL.
Demo — create a list at https://goli.st, or view https://goli.st/demo.
Everything runs against the Firebase emulator suite, so no cloud credentials are needed and production data is never touched.
npm run install:all && npm run demoThat starts the emulators, seeds a few collections, and opens the app on
http://localhost:3000. Requires Node 22+, Java 21 (for the Firestore emulator),
and npm install -g firebase-tools.
Seeded routes worth clicking: /web-mobile (populated), /empty-list (empty
state), /nope-nope (not found).
| Path | What it is |
|---|---|
frontend/ |
React 18 + Vite SPA, Redux Toolkit + redux-saga |
functions/ |
Cloud Functions v2 — link metadata, Domains mirror, OG renderer |
firestore.rules |
Authorization for every read and write |
tests/ |
Security-rules tests against the emulator |
scripts/ |
Local demo, emulator seeding, build glue |
firebase.json at the repository root covers hosting, functions, rules,
indexes, and emulators.
DataGroups/{shortUrl} holds one collection:
Domains/{shortUrl} is a denormalized mirror powering "My Lists". It is
written only by Cloud Functions triggers — clients have no write access.
Every collection has an owner: the client signs in anonymously when no account
is present, which is what makes the ownership checks in firestore.rules
enforceable. ownerId: "PUBLIC" marks shared demo lists, editable by any
signed-in user but deletable by none.
Short URLs are claim-once. Creation runs in a transaction and the rules allow
create but not overwrite, so an existing list can never be clobbered.
npm run demo # full local stack with seed data
npm test # rules + frontend tests
npm run test:rules # security rules only (starts its own emulator)
npm run build && npm run test:api # callable/SSR/mirror tests in disposable emulators
npm run lint # frontend + functions
npm run build # production build + sync the SSR shellPer-package: npm --prefix frontend run dev|build|test|lint,
npm --prefix functions run test|lint.
CI deploys on push to main — rules, indexes, functions, and hosting. To
deploy by hand you need firebase login first:
npm run deploy:preview # throwaway preview channel, expires in 7 days
npm run deploy:rules # firestore rules + indexes
npm run deploy:functions # cloud functions
npm run deploy:hosting # build + hosting (production)Deploy rules before functions before hosting, so that tightened rules are never live against a client that predates them.
Callables enforce App Check in production. Enforcement is behind the
ENFORCE_APP_CHECK environment variable so it can be staged:
functions/.env.localsets it tofalsefor the emulator, which has no attestation provider. With enforcement on, the functions framework rejects every callable withunauthenticatedbefore the handler runs.- To roll out gradually, deploy once with
ENFORCE_APP_CHECK=false, confirm clients are sending tokens, then remove the override.
frontend/.env holds the Firebase web config. Those values are public by
design — they identify the project and authorize nothing; access control lives
in firestore.rules and App Check. Override them in frontend/.env.local
(git-ignored) to point at a different project.
The root and frontend overrides keep Firebase's pinned @grpc/grpc-js dependency
on a patched 1.x release. Remove them when Firebase ships version 1.13.6 or later.
All three packages use Vitest 4 and a patched Vite 6 release. Tailwind 4 removes
the unpatched braces build dependency; its tokens now live in src/index.css
inside the frontend, and the app requires Safari 16.4+, Chrome 111+, or Firefox 128+.
{ "id": "my-list", "title": "Weekend reading", "ownerId": "<uid> | PUBLIC", "items": { "<itemId>": { "id", "link", "title", "snippet", "imageUrl", "order" } } }