This is the public class listing and registration site for Asmbly Makerspace, served at classes.asmbly.org.
Members browse + waitlist + request classes, and are routed to Neon to register for classes. There's also a WIP page where users can authenticate with Neon in order to cancel class registration. Class data is sourced from Neon CRM and kept in sync via a scheduled job.
.
├── app/ # SvelteKit application (the site itself)
├── cron-service/ # AWS Lambda jobs: Neon sync + weekly reporting
├── dev/ # Local dev DB tooling (see dev/README.md)
└── .github/workflows/ # CI: build/push images to ECR, deploy Lambda
- Web app: SvelteKit on the Node adapter, Vite, Tailwind CSS, DaisyUI
- Hosting: AWS App Runner (web app), AWS Lambda with EventBridge (recurring jobs written in node)
- Database: PostgreSQL on Amazon RDS, accessed via Prisma; both the app and recurring jobs connect to it via the same DSN (
DATABASE_URLon App Runner, SSM/classes-db/dsnon Lambda) and share the same Prisma schema - Email: Nodemailer (Gmail SMTP)
- CI/CD: GitHub Actions → AWS ECR → App Runner / Lambda
Runs on AWS App Runner, which auto-pulls the classes-page image from ECR on each push to main. App Runner handles HTTPS at classes.asmbly.org, and routes traffic to the SvelteKit Node server (port 3000).
Routes (app/src/routes/(data-pages)/):
/— search and filter all visible classes (category, sort, group-by-type)/event/[eventTypeId]— class detail page with all upcoming instances/my-classes— WIP: Neon CRM OAuth flow that lets users view their registrations and request cancellations
Internal API:
POST /api/class-registration— Neon webhook; bumps attendee count; marks the registrant's own waitlist request as fulfilled if they had onePOST /api/class-cancellation— Neon webhook; decrements attendee count; emails all waitlist requesters about the open seat (first-come-first-served)
cron-service/ is packaged as a Lambda container image, deployed to the classes-crons Lambda function, and invoked from AWS EventBridge (it is no longer actually a cron job). The handler reads cronType from the event payload and dispatches:
cronType |
Schedule (CT) | What it does |
|---|---|---|
hourlyClassMaintenance |
Every hour, :30 | Pulls active events from Neon, upserts instances, prunes deleted events |
weeklyReportingMetrics |
Sunday 07:00 | Emails classes@asmbly.org a digest of waitlist / on-demand / notify requests |
Secrets are pulled at runtime from AWS SSM Parameter Store.
Note on schema duplication: cron-service/prisma/schema.prisma and cron-service/prisma/migrations/ are byte-identical copies of the ones under app/prisma/ so the cron image can run prisma generate at build time. Schema changes have to be copied into both folders, or the Lambdas will silently go stale.
Defined in app/prisma/schema.prisma:
NeonEventType— class template (e.g. "Woodworking 101")NeonEventInstance— a specific scheduled session (date, teacher, capacity, price)NeonEventCategory/AsmblyArchCategory— categories for classes, Arch being more general (CNC Router -> Woodworking)NeonEventTeacher— instructor recordsNeonEventInstanceRequest— waitlist for a specific instanceNeonEventTypeRequest— "notify me when scheduled" / on-demand requests for a typeNeonEventRequester— email/name of a requesterNeonEventInstanceCancellee— tracks who has cancelled (prevents double-decrement)User+Session— local auth records keyed byneon_id
Migrations live in app/prisma/migrations/ and are applied by prisma migrate deploy at container startup, so DB migrations are applied on every deploy.
See dev/README.md for setting up the website to run locally.
Run from app/. Other scripts in package.json are container-internal or unused.
| Script | Purpose |
|---|---|
npm run lint |
Prettier check + ESLint |
npm run format |
Prettier write |
npm test |
Vitest (currently only a placeholder test) |
npm run dev |
Vite dev server — usually invoked by dev/load-dev-db.sh |
The webapp expects the following (referenced from app/src/lib/server/secrets.js and route handlers). In production these are set on the App Runner service; in local dev, set them in your shell or in app/.env. Note that the recurring Lambda jobs do not read these from .env; they pull equivalents from SSM at invocation time.
Database
DATABASE_URL— Prisma connection string (RDS endpoint in prod; the local Postgres URL in dev, exported automatically bydev/load-dev-db.sh)
Neon CRM — REST API (server-to-server)
NEON_API_KEY,NEON_API_USER— HTTP Basic auth used byapp/src/lib/helpers/neonHelpers.jsto look up registrants (when a registration webhook fires) and fetch a user's registrations (on/my-classes)
Neon CRM — OAuth 2.0 (WIP user login)
CLIENT_ID,CLIENT_SECRET,REDIRECT_URI— used by the/my-classes/login/neonflow. The OAuth token is parsed as the user's Neon ID and stored on the localUserrow; the app never makes further API calls with it.REDIRECT_URImust match exactly what's registered with Neon, e.g.https://classes.asmbly.org/my-classes/login/neon/callback
Neon CRM — webhooks
INTERNAL_API_KEY— shared secret validated on/api/class-registrationand/api/class-cancellation; Neon sends it in the webhook payload'scustomParameters
GMAIL_USER,GMAIL_PASS— Gmail account + app password used bygmailEmailFactory.jsfor transactional emails (waitlist openings, private-session requests)
Newsletter
FLO_API_KEY— Flodesk API key for the footer newsletter signup
.github/workflows/:
build-and-push.yml— on push tomaintouchingapp/**: build linux/amd64 image, push to ECR repoclasses-pagetaggedlatestand${sha}, with BuildKit cache pushed to abuildcachetag.build-cron.yml— on push tomaintouchingcron-service/**: build linux/arm64 image, push to ECR repoclasses-cron, then deploy to Lambda functionclasses-cronsviaaws-actions/aws-lambda-deploy@v1.1.0(timeout 600s).
Both run in us-east-2 and use AWS credentials from repo secrets.
These root-level files and folders are leftovers from the previous deployment model (docker-compose + Caddy on a single host with self-hosted Postgres). They are not used by the current App Runner setup and can safely be ignored — they're listed here so future contributors know not to follow them as a guide:
compose.yaml— orchestrated app + db + cron + caddy on a single host. Replaced by App Runner (web), Lambda (cron), and RDS (database).compose.dev.yaml— earlier local-dev setup that ran the app inside a container. Local dev now runs the app on the host viadev/load-dev-db.sh.caddy/— Caddyfile that terminated TLS and reverse-proxied toapp:3000. App Runner now handles TLS and ingress.hooks.server.js(repo root) — abandoned scaffolding for a Redis-backed page cache. Never wired into SvelteKit (the live hook isapp/src/hooks.server.js, which handles auth) and theredisdependency was never added toapp/package.json.postgres_backup.sh— one-line script that backed up the compose-managed Postgres container by name. Superseded by RDS automated snapshots.