A local events platform for Sarasota, FL, focused on aggregating, organizing, and presenting events in a clean, user-friendly way.
This repository is a monorepo containing:
- a Next.js + React + TypeScript frontend
- a FastAPI + Python backend
- a PostgreSQL database (via Docker)
- Celery for background tasks (event ingestion, scrapers)
- shared tooling for linting, formatting, and development
- Next.js 16 (React 19 + TypeScript)
- Tailwind CSS v4
- Decap CMS (headless CMS for articles)
- Runs on
http://localhost:3000
- FastAPI
- SQLAlchemy 2.0 (ORM)
- Alembic (database migrations)
- Celery (background tasks)
- Redis (message broker for Celery)
- PostgreSQL 16
- Runs on
http://localhost:8000
- Caddy (reverse proxy, automatic HTTPS in production)
- Docker Compose (database, full dev stack, production)
- Flower (Celery monitoring at
http://localhost:5555in dev)
- pnpm (monorepo package manager)
- Ruff (Python linting & formatting)
- pre-commit (git hooks)
srq-hpn/
βββ apps/
β βββ web/ # Next.js frontend
β βββ api/ # FastAPI backend
βββ caddy/ # Caddy reverse proxy config (production)
βββ db/ # PostgreSQL init scripts
βββ docs/ # Documentation (database, Celery, etc.)
βββ compose.yml # Production stack (Caddy, Web, API, DB, Redis, Celery)
βββ compose.dev.yml # Development stack (+ Redis, Celery, Flower)
βββ compose.db.yml # Database only (for local dev without Docker)
βββ pnpm-workspace.yaml
βββ package.json # Root scripts
βββ README.md
Requires .env.local with database credentials. See docs/database-guide.md for details.
pnpm docker:devThis starts: DB, Redis, API, Celery worker, Celery beat, Flower, and Web.
- Web: http://localhost:3000
- API: http://localhost:8000
- Flower (Celery): http://localhost:5555
The app uses a reverse proxy (Caddy) in Docker environments. Route ownership is:
- FastAPI public API:
/api/*(for example/api/events/range) - Next.js route handlers:
/content-api/*(for example/content-api/articles)
Why this split exists:
- Both Next.js and FastAPI can define
/api/*routes. - Reserving
/api/*for FastAPI avoids conflicts and keeps backend endpoints conventional. - Internal Next.js handlers use
/content-api/*to stay explicit and collision-free.
In Docker dev:
- Open the app at
http://localhost:3000(served through Caddy). - Browser calls to
/api/*are proxied to FastAPI. - Browser calls to
/content-api/*are handled by Next.js. http://localhost:8000is still exposed for direct API debugging.
# 1. Start database
pnpm db:up
# 2. Create API venv and run migrations (see apps/api/)
pnpm dev:api
# 3. In another terminal: frontend
pnpm dev:webAdmin routes are protected with role-based auth (admin, user) using a
first-party httpOnly cookie.
Set these API env vars in your runtime env:
-
Docker dev (
compose.dev.yml):./.env.local -
API-only local runs (
pnpm dev:api):apps/api/.env(or shell env) -
JWT_SECRET(required) -
JWT_ALGORITHM(optional, defaults toHS256) -
JWT_EXPIRES_MINUTES(optional, defaults to60) -
COOKIE_SECURE(optional,truein production) -
COOKIE_SAMESITE(optional, defaults tolax)
Production expects a same-origin deployment (frontend + /api behind a reverse
proxy). Keep COOKIE_SAMESITE=lax and COOKIE_SECURE=true.
Create your first admin user:
python apps/api/scripts/create_admin_user.py --email admin@example.com --password "change-me-now"For full production setup, use docs/database-guide.md as the source of truth. This section gives the minimum launch-critical steps.
In your production env file or secret manager, set at least:
DOMAIN=srqhappenings.com
NEXT_PUBLIC_API_BASE_URL=
NEXT_PUBLIC_SITE_URL=https://srqhappenings.com
NEXT_PUBLIC_UMAMI_WEBSITE_ID=<umami_website_id>
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://cloud.umami.is/script.js
NEXT_PUBLIC_ANALYTICS_DEBUG=false
POSTGRES_PASSWORD=<secure-value>
POSTGRES_APP_PASSWORD=<secure-value>Why NEXT_PUBLIC_SITE_URL matters:
- It powers canonical URLs, sitemap URLs, robots host/sitemap references, and JSON-LD absolute URLs.
- If this is wrong, SEO signals can point to the wrong origin.
Why Umami vars matter:
NEXT_PUBLIC_UMAMI_WEBSITE_IDenables client-side analytics capture in the web app.NEXT_PUBLIC_UMAMI_SCRIPT_URLpoints to your Umami tracking script host (Cloud default shown above).- If
NEXT_PUBLIC_UMAMI_WEBSITE_IDis missing, analytics capture is safely disabled. - Set
NEXT_PUBLIC_ANALYTICS_DEBUG=truelocally to print each tracked event to the browser console.
./scripts/deploy-prod.sh .env.production mainThis script performs the production-safe sequence automatically:
- Pull latest branch
- Validate compose config
- Build images
- Start
db+redisand wait for readiness - Run
alembic upgrade head - Start full stack (
caddy,web,api,db,redis,celery-worker,celery-beat) - Run post-deploy health checks
cp .env.production.example .env.production
# edit .env.production with real secrets and domain values
chmod +x scripts/deploy-prod.sh scripts/check-prod-health.sh./scripts/check-prod-health.sh .env.productioncurl -I https://srqhappenings.com
curl https://srqhappenings.com/api/health
curl https://srqhappenings.com/robots.txt
curl https://srqhappenings.com/sitemap.xmlThen complete the SEO launch checks in docs/seo-implementation-checklist.md (Search Console property + sitemap submission + URL inspection).
Step 4 launch analytics is now wired in via the Umami browser script in the web app.
Tracked launch events:
event_viewedevent_link_clickedfeatured_event_impressionfeatured_event_clicked
Current instrumentation points:
- Homepage featured card impression and click
- Event detail page view
- Event detail external link click
Common tracked properties:
event_id,event_slug,event_titlesource,source_page,source_componentvenue_id,venue_slug,venue_name(only when present)
Quick verify:
- Set
NEXT_PUBLIC_UMAMI_WEBSITE_IDand optionallyNEXT_PUBLIC_UMAMI_SCRIPT_URL. - Open homepage and click featured event card.
- Open an event detail page and click the outbound event link.
- Confirm those events appear in your Umami event dashboard.
Local-only debugging:
- Set
NEXT_PUBLIC_ANALYTICS_DEBUG=truein local env. - Keep
NEXT_PUBLIC_UMAMI_WEBSITE_IDunset to avoid sending events while still seeing console logs.
| Command | Description |
|---|---|
pnpm dev:web |
Start Next.js dev server |
pnpm dev:cms |
Start Next.js + Decap CMS |
pnpm dev:api |
Start FastAPI with uvicorn |
pnpm lint:web |
Lint frontend |
pnpm lint:api |
Lint API (Ruff) |
pnpm format:api |
Format API (Ruff) |
pnpm db:up |
Start Postgres (compose.db.yml) |
pnpm db:down |
Stop Postgres |
pnpm docker:dev |
Start full dev stack |
pnpm docker:dev:build |
Build and start dev stack |
pnpm docker:dev:down |
Stop dev stack |
pnpm docker:migrate |
Run migrations (dev container) |
When adding a new event source, use a stable slug (not numeric IDs) so Celery scheduling works the same across local and production.
- Add the source row in the database with a unique
sluginsources. - Add a slug constant in
apps/api/app/constants/sources.py. - Wire the source into task scheduling in
apps/api/app/celery_app.pyusingsource_slugin task kwargs. - Ensure the corresponding task in
apps/api/app/tasks.pyresolves via_resolve_source(...)(slug-first,source_idfallback). - If the source has a new collector module, implement it under
apps/api/app/collectors/and register any required task wrapper inapps/api/app/tasks.py. - Run migrations and restart workers/beat:
docker compose --env-file .env.production -f compose.yml exec api alembic upgrade head
docker compose --env-file .env.production -f compose.yml restart celery-worker celery-beatQuick verification query:
SELECT id, slug, name, type FROM sources ORDER BY id;Manual task call example (slug-based):
docker compose --env-file .env.production -f compose.yml exec celery-worker \
celery -A app.celery_app call app.tasks.collect_asolorep \
--kwargs='{"source_slug":"asolorep","delay":3.0}'