The official website for STEM Sprouts, a youth-led nonprofit building a global network of student-run STEM chapters, powered by Pinboard, our open-source learning platform.
/: Home/about: Mission, vision, and team/locations: Chapter map and locations/partner: Sponsorship and partnership info/pinboard: Our open-source learning platform/resources: Curriculum and learning resources/news: News & research posts (includes the NSRI x STEM Sprouts Research Partnership updates)/admin: Login-gated post editor for/news
npm install
npm run devThen open http://localhost:3000. Note this may vary
npm run build
npm run startThe /news blog and /admin editor are backed by Supabase, with a JWT cookie for admin auth.
Admin accounts (username + bcrypt password hash) live in the admin_users table, not an env var.
- In the Supabase project's SQL editor, run
supabase/schema.sqlto create theposts,admin_users, andadmin_invitestables. Also create a public Storage bucket namedpost-images(Storage > New bucket > Public), used for post cover images.- If these tables already exist (the deployed project), instead run just the "MIGRATION FOR EXISTING DATABASES" block at the bottom of that file — it adds admin roles and the post review workflow without touching existing data.
- Fill in
.env.local(see.env.localfor the placeholders already there):SUPABASE_SERVICE_ROLE_KEY— Supabase dashboard > Project Settings > API > service_role secret. Server-only, never expose to the client.JWT_SECRET— already generated; keep it secret and consistent across environments (dev + Vercel).ADMIN_SETUP_SECRET— already generated; a one-time code only the site owner should know, required to claim the first admin account (see below). Keep it out of chat/Slack once you've used it — treat it like a password.NEXT_PUBLIC_SITE_URL— optional, defaults tohttps://stem-sprouts.org. The canonical origin used to build the sitemap, robots.txt, and RSS feed URLs.GOOGLE_INDEXING_SERVICE_ACCOUNT_JSON— optional. The full JSON key (as a single-line string) for a Google Cloud service account with access to the Indexing API. When set, publishing a post automatically pings Google to crawl it. See "Google Search Console & Indexing API" below.
- Set the same env vars in Vercel (Project Settings > Environment Variables) for production.
- Visit
/admin/setupand create the first admin account (you pick your own username/password), enteringADMIN_SETUP_SECRETin the "Setup code" field. This only works once — it's how whoever runs this first claims the first account, so nobody else can beat them to it. The first account created this way is always a main admin. - To add more admins, log in and go to
/admin/invite— pick a role (Main Admin or Chapter Lead), generate a one-time link, and send it to them. They open it and pick their own username/password; you never see or set it for them. Links expire after 7 days or first use.
- Main admin — full access: publish posts directly, review and approve/reject chapter leads' posts, invite new admins, and remove admin accounts (
/admin/admins). - Chapter lead — can create and edit their own posts only. Saving always lands the post as "Pending Review" (never goes live directly, including edits to an already-published post of theirs) until a main admin approves it from the "Pending Review" section on
/admin.
Existing admin accounts created before this feature shipped were grandfathered in as main admins by the migration above.
app/sitemap.tsandapp/robots.tsgenerate/sitemap.xmland/robots.txtdynamically — every published post is included automatically, no manual edits needed./news/feed.xmlis an RSS feed of the latest 30 published posts.app/not-found.tsxis the site's 404 page.
These steps need to be done once, by hand, in Google's dashboards — there's no way to automate the initial verification:
- Go to Google Search Console and add
stem-sprouts.orgas a Domain property (verify via the DNS TXT record it gives you — add it wherever the domain's DNS is managed). - Under Sitemaps, submit
https://stem-sprouts.org/sitemap.xml. - For the Indexing API ping (
lib/google-indexing.ts, wired into publish/approve actions inapp/api/posts/*):- In Google Cloud Console, create a project (or reuse one), enable the Web Search Indexing API, and create a Service Account with a JSON key.
- In Search Console (Settings > Users and permissions), add that service account's email as an Owner of the
stem-sprouts.orgproperty. - Set
GOOGLE_INDEXING_SERVICE_ACCOUNT_JSON(the full key file contents, minified to one line) in.env.localand in Vercel. Until it's set, publishing just logs a console warning and skips the ping — it never blocks a publish.
- To confirm it's working: publish a post, then check it in Search Console's URL Inspection tool — it should show as crawled within a day or two.
Anyone can subscribe on /news (components/newsletter-signup.tsx → POST /api/newsletter/subscribe). Whenever a post is newly published — directly or via a main admin approving a chapter lead's pending post — everyone subscribed gets emailed automatically (lib/newsletter.ts, wired into the same three places as the Indexing API ping in app/api/posts/*). Editing a post that's already live does not re-trigger an email.
Subscribers and sends are managed entirely by Resend — there's no subscriber table in Supabase. The first subscribe (or the first publish, whichever happens first) auto-creates a Resend Segment named "STEM Sprouts News"; new subscribers get added to it, and publishing creates + sends a Broadcast to that segment. Unsubscribe links are handled by Resend automatically.
Setup:
- In Resend, add and verify the sending domain (
news.stem-sprouts.org— needs DNS records added wherever the domain is managed). - Create an API key (Resend dashboard > API Keys) and set
RESEND_API_KEYin.env.localand in Vercel. - Optionally set
RESEND_FROM_EMAILto override the default sender (STEM Sprouts <no-reply@news.stem-sprouts.org>). - Until
RESEND_API_KEYis set, both the signup form and the send-on-publish hook no-op safely (signup returns a "not set up yet" error; publishing just skips the email) — neither blocks a publish.