An open platform for conservation science, field discoveries, and community action. Contributors publish field reports and research; readers follow, discuss, and share them.
- 📝 Publish and manage posts on environmental topics
- 🧭 Browse and filter by topic, keyword, and popularity
- 🔐 Supabase email/password auth with profile editing and avatars
- 💬 Threaded comments with realtime updates and image attachments
- 🧮 Recommendation engine — PageRank, collaborative filtering, and pgvector similarity over a hybrid ranking
- 🔎 Full-text and vector search backed by Postgres
- 🖼️ Responsive, accessible dark UI
Frontend — Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS 4, lucide-react icons, EasyMDE for post authoring.
Backend — Supabase (Postgres, GoTrue auth, Storage),
pgvector for embeddings, pg_trgm and tsvector for search.
Recommendation engine — a Rust service in pagerank/ that recomputes
PageRank over the interaction graph and writes through to Postgres and Upstash Redis.
It is currently switched off in CI by a flag file, so nothing is recomputed until
that is enabled — see The engine is switched off by a flag file.
CI/CD — GitHub Actions runs lint, build, database rehearsal, and Rust checks on
every push. Vercel is connected to the repository and builds
from GitHub on its own: every pull request gets a preview deployment, and main
goes to production. There is no deploy workflow, so nothing in the repository
holds a Vercel token.
- Node.js 20.9+ (see
enginesinpackage.json) - Docker — only needed for the local database rehearsal (
npm run db:verify)
git clone https://github.com/Gauravpawar101/NatureIndex.git
cd NatureIndex
npm install
cp .env.example .env.local # then fill in your Supabase keys
npm run devOpen http://localhost:3000.
npm run check:envValidates both env files, checks the URL and key shapes, resolves the project
hostname in DNS, and calls the auth endpoint with your key. Run this first
whenever login or uploads fail — the browser only ever reports Failed to fetch, which cannot distinguish a wrong URL from a deleted project from an ad
blocker. It exits non-zero on failure, so it is also usable in CI.
Apply supabase/schema.sql in the Supabase SQL editor
(Dashboard → SQL Editor → New query → paste → Run). It is fully idempotent, so
re-running it is safe — you do not have to track which sections you have applied.
Then seed realistic content:
npm run seed # upsert authors, posts, comments, recommendation scores
npm run seed -- --reset # wipe the seeded rows first, then reseedThe seeder needs SUPABASE_SERVICE_ROLE_KEY in .env.local (RLS correctly blocks
the anon key from writing). It is idempotent too — every write is keyed on a
natural identifier, so re-running refreshes rows instead of duplicating them.
Demo login after seeding: demo@example.com / DemoPassword123!
⚠️ The demo credentials are committed in the repository. Rotate or remove them before seeding anything you care about.
Uploads need three public buckets, which supabase/schema.sql creates:
avatars, post-images and comment-images.
Each has a file_size_limit and an allowed_mime_types list that must stay
in sync with UPLOAD_TARGETS in src/app/lib/uploads.js.
The client validates a file against those numbers before sending it, so a bucket
limit stricter than the app's own turns into an opaque storage error instead of
a readable "too large" message.
Objects are stored as <userId>/<timestamp>-<random>.<ext>. The first path
segment must be the user id, because the RLS delete policies authorise
removal with (storage.foldername(name))[1] = auth.uid(). Note that
split_part(name, '-', 1) is not equivalent — it splits on hyphens, and a
UUID is full of them, so it returns only the first fragment of the id and never
matches. supabase/tests/verify.sql asserts against that regression.
npm run db:verifyThis starts a throwaway pgvector/pgvector:pg16 container, applies the local
fixture, then supabase/schema.sql twice (to prove it is re-appliable),
loads the seed data, and runs the assertion suite in
supabase/tests/verify.sql. No credentials and no
network access required — the same command CI runs.
| Command | What it does |
|---|---|
npm run dev |
Development server |
npm run build |
Production build |
npm start |
Serve the production build |
npm run lint |
ESLint across the repo |
npm run check:env |
Diagnose the Supabase configuration and connectivity |
npm run db:verify |
Local schema + seed rehearsal in Docker |
npm run seed |
Seed the connected Supabase project |
npm run seed:sql |
Emit the seed dataset as SQL on stdout |
Rust engine (pagerank/pageRankEngine):
cargo test # PageRank unit tests
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo run --release # requires DB_URL and REDIS_URL| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
pushes and pull requests | lint, production build, database rehearsal, Rust clippy + tests |
pagerank.yml |
every 15 min, on engine changes | recompute PageRank and write through to Postgres + Redis — off by default, see below |
pagerank.yml never runs the engine unless pagerank/flags.json
says so:
{ "pagerankEnabled": false }Every trigger — the 15-minute schedule, workflow_dispatch, and pushes under
pagerank/** — goes through a flag job first, and the compute job is
skipped when the flag is false. To turn the engine on, set it to true and
push: pagerank/** already matches the flag file, so the same push that flips
it also triggers a run. Nothing else is needed, and no GitHub setting or secret
is involved.
Two deliberate details. A malformed flag (missing file, missing key, a value
other than true/false) fails the run instead of being read as false, so
a typo cannot masquerade as a deliberate switch-off. And the schedule still
fires every 15 minutes while disabled — it costs one short-lived runner for the
flag job, and skipping the schedule itself is impossible, because path filters
do not apply to schedule events. ci.yml keeps running clippy and the unit
tests on every push regardless, so the engine does not rot while it is off.
The gate lives in the workflow, not in the binary: cargo run --release locally
still runs whenever you invoke it, because the flag is a CI concern and reading
it from the working directory would make a deployed build depend on where it
happens to be started from.
| Secret | Used by |
|---|---|
DB_URL, REDIS_URL |
pagerank.yml |
No secrets are needed for ci.yml — it builds with a placeholder Supabase URL
and runs entirely against a local container. No secrets are needed for
deployment either, because Vercel holds its own token and reads the repository
directly.
CI is advisory, not a gate. Vercel builds from Git as soon as a commit lands, without waiting for
ci.yml, and blocking on required status checks is a paid feature. So a migration that breaks the database rehearsal can still reach production. Watch theci.ymlresult on each pull request and do not merge a red one. If you would rather have the rehearsal genuinely block, the alternative is deploying fromdeploy.ymlwith aVERCEL_TOKENsecret and giving that workflow anif: success()on the CI jobs.
Vercel's Git integration does the deploying, so there is no deploy workflow to maintain and no CI secrets to rotate.
- In Vercel, Add New → Project and import this repository. The framework
preset is detected as Next.js; accept the defaults for install, build
(
npm run build) and output. - Add the environment variables below under Settings → Environment Variables.
Mark the two
NEXT_PUBLIC_SUPABASE_*variables for all environments. - Deploy. Every later push to
maindeploys to production, and every pull request gets a preview URL automatically.
| Variable | Scope | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
build + runtime | Project URL from Supabase → Project Settings → Data API |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
build + runtime | The publishable (sb_publishable_…) or legacy anon key |
NEXT_PUBLIC_URL |
runtime | The production origin, e.g. https://natureindex.vercel.app. Falls back to the request origin, but email confirmation links are built from this, so set it once the domain is known. |
SUPABASE_SERVICE_ROLE_KEY |
runtime, server only | Never prefix this with NEXT_PUBLIC_; it bypasses row level security. Only needed if you run the seeder against the deployed project. |
NEXT_PUBLIC_* values are inlined into the client bundle at build time, so a
value that is missing from the build environment is baked into the JavaScript and
fixing it later requires a rebuild, not just a restart. A build with no Supabase
configuration succeeds and renders a "Supabase is not configured" notice, which
is deliberate: it fails loudly at the UI rather than as a Failed to fetch.
Vercel's Hobby plan is for personal, non-commercial use, and hard-caps usage instead of billing it. This site is well inside every cap, but a project that earns revenue needs Pro.
src/app/ routes, components, and the Supabase data layer
scripts/
seed-data.mjs the seed dataset (authors, posts, comments)
seed.mjs idempotent seeder, talks to Supabase
seed-sql.mjs emits the same dataset as SQL, for CI
check-env.mjs Supabase config + connectivity diagnostics
verify-db.sh local/CI database rehearsal
supabase/
schema.sql tables, RLS policies, functions, views, pgvector
tests/local_fixture.sql stand-ins for Supabase-managed schemas
tests/verify.sql assertion suite
pagerank/pageRankEngine/ Rust PageRank + hybrid ranking service
Failed to fetch on login, or a permanent "Cannot reach the sign-in service"
banner. The request never left the browser. Run npm run check:env. If it
reports that the project hostname does not resolve, NEXT_PUBLIC_SUPABASE_URL
points at a project that no longer exists — copy the current Project URL from
Supabase → Project Settings → Data API and restart the dev server. Retyping
credentials will not help, because they are never sent.
Sign-in works, uploads fail with "bucket not found". The storage buckets
have not been created. Re-run supabase/schema.sql. Note that the bucket
insert is on conflict do update, so re-running also pushes changed size and
MIME limits to a project that already has the buckets.
Uploads fail with a size error for a file the app says is fine. The bucket's
file_size_limit disagrees with UPLOAD_TARGETS. Re-run supabase/schema.sql
to resync, and keep the two in step in future.
A contributor cannot delete their own avatar or comment image. The delete
policies are the pre-fix version using split_part. Re-run
supabase/schema.sql; npm run db:verify asserts the corrected form.
"That sign-in link did not work" right after clicking an email link. The
auth callback could not exchange the code. Check NEXT_PUBLIC_URL — it must be
the origin the email links point at, not an internal address, which is a common
problem behind a reverse proxy or on preview deployments.
Edits to .env.local have no effect. Next.js reads env files at startup.
Restart the dev server.
All rights reserved.