A privacy-first vegan community and activism platform for Southern California.
SocialTypeScript · CSSGitHub @wbaxterhOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Vegan Grove web: Next.js 15 public site and member area for a privacy-first vegan community and activism platform. MapLibre maps, httpOnly sessions, no analytics. Hosted on AWS Amplify.
Taken from the repository README
Loading the full README.
Read the full README ↗Public pages rendered on the server, a member area behind an httpOnly cookie, and no session token in the browser.
Vegan Grove is a privacy-first vegan community and activism platform for Southern California. This repository is the website at vegangrove.org: a Next.js 15 App Router application that renders the public places, events, groves, media, and guides pages on the server and serves the authenticated /app area for the feed, messages, friends, the companion, and settings. It owns no data and talks to nothing but the Vegan Grove API and OpenFreeMap tiles.
| Repository | Role | Stack | Deploys to |
|---|---|---|---|
| vegan-grove-api | REST API, Socket.IO, workers, the only reader of the database | Express 5, Mongoose 9, zod, pino, Socket.IO, vitest | One EC2 instance, PM2 behind nginx, us-east-1 |
| vegan-grove-web (this repo) | Public site and the /app member area | Next.js 15, Tailwind 4, shadcn/ui, MapLibre GL | AWS Amplify Hosting, us-east-1, on push to main |
| vegan-grove-mobile | iOS and Android app | Expo SDK 57, expo-router, TanStack Query, MapLibre | EAS Build, App Store and Play |
| vegan-grove-docs | Product, privacy, and architecture docs | PokeDocs on Docusaurus 3 | AWS Amplify Hosting, us-east-1, on push to main |
Docs: docs.vegangrove.org. Product: vegangrove.org. Both domains and api.vegangrove.org are launching.
The browser never holds the session. It posts credentials to a Next route handler, the handler exchanges them with the API, and the token comes back only as an httpOnly cookie. Server components forward that cookie as a Bearer header; client components call the same API for public data and are never handed a token.
sequenceDiagram
participant B as Browser
participant N as Next 15 (Amplify)
participant A as API
participant T as OpenFreeMap tiles
B->>N: POST /api/session (mode, email, password)
N->>A: POST /api/auth/login
A-->>N: token and user
N-->>B: Set-Cookie vg_session (httpOnly, Secure, SameSite=Lax) and the user as JSON
Note over B,N: The token never reaches browser JavaScript
B->>N: GET /app/feed with the cookie
N->>N: middleware checks that the cookie exists
N->>A: GET /api/me with Authorization Bearer, from a server component
A-->>N: user
N-->>B: rendered HTML
B->>T: vector tiles for /places (no key, no cookie)
B->>A: GET /api/places?bbox=w,s,e,n (public)
B->>N: DELETE /api/session
N->>A: POST /api/auth/logout
N-->>B: cookie cleared
Public pages are server components that call the API without a session and revalidate every 60 seconds; when the API is down they render an honest empty state instead of a 500. Pages under /app are rendered per request because they read the cookie. The map is the only page that is client-only from the top, because MapLibre needs window; its tile worker is self-hosted under public/maplibre/ because Turbopack hashes the worker and its shared chunk separately, which breaks the worker's relative import and leaves the map blank.
nvm use # Node 24, from .nvmrc
npm ci
cp .env.example .env.local # the dev defaults work against a local API on :4000
npm run dev # next dev --turbopack, http://localhost:3000
npm run validate # biome check, tsc --noEmit, next build
npx playwright install chromium # once, then:
npm run test:e2e # Playwright against a mock API, see tests/fixtures
The site expects vegan-grove-api at NEXT_PUBLIC_API_BASE_URL (default http://localhost:4000/api) and runs without it. Husky runs Biome on staged code and secretlint on every staged file.
Playwright starts two servers of its own: the mock API on port 4010 (MOCK_API_PORT) and a Next dev server on port 3000 (PLAYWRIGHT_PORT) pointed at it. It never reuses a dev server you started by hand, because that one points at a different API. New shadcn components come from npx shadcn@latest add <name>, not from hand-written files, so upgrades stay clean.
| Script | What it does |
|---|---|
npm run dev | next dev --turbopack |
npm run build | next build --turbopack, what Amplify runs |
npm run predev, npm run prebuild | copy MapLibre's worker and shared chunk into public/maplibre/ (run automatically) |
npm start | next start on a built app |
npm run lint | biome check . |
npm run lint:fix | biome check --write . |
npm run typecheck | tsc --noEmit |
npm run test:e2e | Playwright in chromium against a mock of the API (tests/fixtures/mock-api.mjs) that serves fixtures in the API's real envelopes and honors the places query params: home, the map container, markers from map-pins, the vegan-level, type, and chain filters and their URL round-trip, the marker popup and the side list, a detail page from { place } with phone, hours, and the map preview, a 404 for an unknown slug, the /app redirect, security headers |
npm run validate | lint, typecheck, build: the CI contract and the PR gate (Playwright stays separate) |
npm run prepare | installs the husky hooks |
Names only in .env.example; never commit a .env file. On Amplify, set these in the console; the amplify.yml preBuild step copies NEXT_PUBLIC_* and SESSION_* into .env.production so next build can inline them.
| Variable | Purpose | Shape |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | API base including /api; its origin is added to the CSP connect-src | URL, dev default http://localhost:4000/api |
NEXT_PUBLIC_MEDIA_CDN_ORIGIN | Origin that serves uploaded images and posters; added to img-src, media-src, and images.remotePatterns | origin, no path |
NEXT_PUBLIC_SITE_URL | Canonical origin for metadata, robots.txt, sitemap.xml, llms.txt | origin, default https://vegangrove.org |
SESSION_COOKIE_SECURE | Force the cookie Secure flag; unset means secure in production only | true or false |
src/
app/
(site)/ public pages under the shared header and footer, plus login and signup
app/ member area: the layout validates the session, then feed, messages, friends, companion, settings
api/session/route.ts the only code that sees the session token; sets and clears vg_session
llms.txt/ robots.txt/ sitemap.xml/ route handlers built from src/lib/site.ts
globals.css the --vg-* tokens mapped onto shadcn's variables; no other file defines a color
layout.tsx html, ThemeProvider (dark by default), system font stacks
components/
ui/ shadcn components on Base UI, regenerated with the shadcn CLI, not edited by hand
places/ places-map.tsx (MapLibre, client only) and its dynamic-import island
auth/ app/ events/ media/ forms, member nav, cards, the youtube-nocookie trailer embed
screen.tsx the Context / Action / Support layout every page uses
lib/
api.ts apiFetch, the one HTTP client for browser and server
api.server.ts forwards the cookie as a Bearer header for server components
session.ts cookie name and options, shared with the middleware
loaders.ts public-page loaders that never turn an API outage into a 500
middleware.ts redirects /app/* without a cookie to /login?next=
public/maplibre/ MapLibre worker files, copied on predev and prebuild, gitignored
scripts/ copy-maplibre-worker.mjs
tests/ smoke.spec.ts (Playwright)
next.config.ts CSP, HSTS, nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy
amplify.yml Amplify build: env allowlist into .env.production, npm ci, next build, .next artifacts
/, /places with the MapLibre map island querying GET /api/places?bbox= as the viewport settles, /places/[slug], and the list and detail pages for /events, /groves, /media, /guides. The last four call routes the API still answers with 501, so they render their empty states until the API lands them./login (password, plus a magic-link request) and /signup post to /api/session, which proxies the API and sets the cookie; DELETE /api/session signs out and revokes the session upstream.middleware.ts bounces /app/* without a cookie, the /app layout validates the session with GET /api/me and bounces expired cookies the same way, and /app/settings shows the member's handle, home area, and public-posts switch with a working sign out./privacy, /terms (an outline), robots.txt, sitemap.xml from the public route list, llms.txt, a not-found page, dark and light themes./app/feed, /app/messages, /app/friends, and /app/companion are shells with disabled controls and the right copy; each carries a TODO(m2) naming the API routes it will call./login until the native and web flows exist./handles/[handle] public-posts view. It is allowed by privacy rule 1 and deliberately not built in the scaffold; when it is, it reads GET /api/handles/:handle/posts and nothing else.'unsafe-inline' for scripts until the app moves to a per-request nonce./messages namespace yet.SameSite=Lax cookie named vg_session, Secure in production, 30 days to match the API's sliding expiry. The session route returns the user, never the token.localStorage, no client code reads the cookie, and api.ts only accepts a token from server code.default-src 'self', allows connect-src to the API origin, OpenFreeMap tiles, and the Bunny CDN only, sets frame-ancestors 'none', and limits frames to youtube-nocookie.com for click-to-load trailers.GET /api/stats. Any new external origin has to be added to the CSP in next.config.ts and justified in the PR.The full promise, data inventory, and threat model: docs.vegangrove.org/privacy.
The code is public so anyone can audit how member data is handled; read CONTRIBUTING.md before opening a PR and SOUL.md before changing a screen. Report vulnerabilities through the process in SECURITY.md, never in a public issue. The LICENSE is proprietary: read it, study it, contribute to it, and do not redistribute it.
Built by Wes Huber · Sibling of The Trick Book · Docs by PokeDocs