This is the work-in-progress version of the open-source SciTeens platform. This application is built in Next JS (A Server-Side Rendered React JS framework.) If you encounter issues with the website, please fork our site and commit fixes or detail the issues in the issues tab on GitHub. If you just want to explore the code, then enjoy!
If you'd like to learn about web development, you can explore this repository and the tools that we use. We've provided the tools that we use below, as well as some good learning guides for getting started with each of these tools.
- Tailwind CSS
Tailwind CSS is a class-based CSS framework that allows us to style our website. It's pretty straightforward to learn, the documentation is fantastic, and you can try it yourself without downloading anything. - React
React is a framework that allows us to create the functionality for each of our pages. To get started, be sure to check out this website - Next JS
Next JS is built on top of React, and allows users to find our website easier via search engines like Google using a concept called Server Side Rendering or Static Site Generation (SSR or SSG). To learn more about Next JS and the concepts of SSR and SSG, check out the Next JS docs. - Firebase
Firebase is a fantastic tool for easily managing the back-end of an application from the front-end. Firebase takes care of managing users, website analytics, as well as storing data in a NoSQL database. To get started with learning Firebase, we recommend checking out this website or watching this video. Also, be sure to check out the official Firebase YouTube channel https://www.youtube.com/c/firebase - Docker
To host our website, we use a tool called Docker paired with Google Cloud Run. These tools allow us to "bundle" our site (almost like we're packaging our website up into a box) and then putting that "box" on Google Cloud Run for other people to access at https://sciteens.com.
Before you begin, make sure that you have both Git and Node JS installed on your computer. To get started with the code, follow the steps below:
- Clone the repository by typing in
git clone https://github.com/Sci-Teens/sciteens.gitinto your command line. If you don't have access, you can fork the repository instead. - Type in
cd sciteensto the command line and hit enter. - Type in
corepack pnpm installto the command line and hit enter. This will download all necessary packages. - Type in
corepack pnpm devand visit localhost:3000 in your browser. This will show the development build! - If you encounter an error at the step above, it's likely because you don't have access to the API keys. If you'd like to join the team to contribute to the website, reach out!
The Discover Opportunity Sources GitHub Actions workflow runs only
when an operator starts it. It has no scheduled trigger.
- Open the repository's Actions tab.
- Select Discover Opportunity Sources.
- Select Run workflow.
- Leave
executeunchecked for a dry run.
If you want to write the results to Firestore, select execute.
The write run creates active sources for accepted programs and records
final rejections. Each run performs a new search. A write run does not
reuse results from a previous dry run.
Discovery uses the scrape-opportunities environment and its existing
GCP variables. Dry runs still call the model and fetch public pages.
The separate Scrape Opportunities workflow continues its weekly
refresh of active sources.
Discovery requires a successful HTTPS page fetch before the model verdict.
An official source URL must match a successful fetch from that verification.
Name mismatches remain eligible for another discovery run, including
previous mismatch rejection records. Discovery decisions use
discoveryReasoning and discoveryRedFlags. Scraper updates retain them.
GitHub Actions schedules the opportunity scraper each Monday at 06:00 UTC. The scraper completes the batch and saves successful results even when another source fails. Any failed source gives the process a nonzero exit status, so GitHub Actions marks the run as failed.
If every prefetched page fails, the scraper stops extraction before the model call. When the transport supplies an error code, the failure record retains it. Public-address checks and URL provenance checks still apply.
Both scripts use the same constrained browser transport. Initial redirects pass through the public-address guard before the browser receives the page. The browser blocks later redirects, service workers, and WebSockets. A blocked main-page redirect causes a fetch failure, not publication.
GitHub Actions schedules the opportunity deadline post each Monday at 10:00 UTC. It selects dated opportunities due in the next 30 days. Each carousel places the nearest deadline first. The workflow creates another ordered carousel when more than nine opportunities qualify.
Create a social-posts GitHub environment. Set these variables:
GCP_PROJECT_IDGCP_WIF_PROVIDERGCP_SCRAPER_SASITE_URL
The Google Cloud Workload Identity provider must accept the identity for
the social-posts environment:
repo:Sci-Teens/sciteens:environment:social-posts
Keep the repository, branch, and workflow restrictions in the provider's
attribute condition. The service account must also grant
roles/iam.workloadIdentityUser to this identity.
The scrape-opportunities environment uses a different identity.
Access for that environment does not authorize social-posts.
If authentication reports The given credential is rejected by the attribute condition.,
inspect the provider's attribute condition before the service account policy.
The scheduler does not start when this check fails.
Set the BUFFER_API_KEY secret. The workflow uses the Directed Relic
Buffer project by default. Set BUFFER_ORGANIZATION_NAME only to use
another project.
If Directed Relic has more than one Instagram channel, set the
BUFFER_CHANNEL_ID secret. The Buffer channel must be connected.
Set the channel posting schedule in Buffer. The workflow uses that
queue, so Buffer selects the next configured posting time.
SciTeens sends the monthly newsletter through Plunk campaigns.
The newsletter uses React Email templates in functions/lib/emailTemplates.js.
The system uses two Plunk static segments:
SciTeens - Transactionalcontains website account contacts.SciTeens - Newslettercontains confirmed newsletter subscribers.
The newsletter segment and Plunk subscription state control newsletter consent. A newsletter opt-out does not stop transactional email delivery.
- Verify the
sciteens.orgsending domain in Plunk. - Create a Plunk secret API key (
sk_*) that can manage contacts, segments, and campaigns. - Set the API key as the Firebase
PLUNK_SECRET_KEYsecret. - Deploy the Cloud Functions.
- Authenticate the local Google Cloud CLI with Application Default Credentials.
- Set
PLUNK_SECRET_KEYin the local shell. - Run the contact migration.
firebase functions:secrets:set PLUNK_SECRET_KEY
firebase deploy --only functions
export GCP_PROJECT_ID=<gcp-project-id>
export PLUNK_SECRET_KEY=<plunk-secret-key>
gcloud auth application-default login
pnpm newsletter:sync -- --project "$GCP_PROJECT_ID"The first sync, confirmation, or campaign that needs it creates the Newsletter segment. The first sync moves website accounts and newsletter records from Firestore into their correct segments. To migrate the existing Resend address book, run the one-time Resend migration with both API keys in your local shell:
export RESEND_APIKEY=<resend-api-key>
export PLUNK_SECRET_KEY=<plunk-secret-key>
pnpm newsletter:migrate-resend -- --project "$GCP_PROJECT_ID"The migration subscribes every Resend contact except contacts already globally unsubscribed in Resend or through the SciTeens newsletter unsubscribe page. The first sync creates replacement unsubscribe tokens only for unmarked newsletter subscribers. Later syncs update segment membership without token rotation.
Create a JSON file for the issue. The file must contain these fields:
namesubjectpreviewtitleopeningfeaturedArticlefeaturedProject
opening is an array of one to three paragraphs.
Each featured item needs title, description, and an HTTPS href.
An optional featured image needs imageUrl and imageAlt.
You can omit opportunities.
When you omit it, the command selects up to six dated opportunities in the next 30 days.
The query uses the same deadline window as the Instagram post process.
Use this shape when you create a file:
{
"name": "Month Year",
"subject": "The subject for this issue",
"preview": "The inbox preview text",
"title": "The newsletter title",
"opening": [
"The first opening paragraph.",
"The optional second opening paragraph."
],
"featuredArticle": {
"title": "The article title",
"description": "The article description",
"href": "https://sciteens.org/article/<slug>"
},
"featuredProject": {
"title": "The project title",
"description": "The project description",
"href": "https://sciteens.org/project/<slug>"
}
}First, create an HTML preview.
pnpm newsletter:create -- \
--input monthly-newsletter.json \
--project "$GCP_PROJECT_ID" \
--dry-runThe command writes an HTML file beside the JSON file. Read the HTML file before you create a Plunk draft.
Then create a draft broadcast.
pnpm newsletter:create -- \
--input monthly-newsletter.json \
--project "$GCP_PROJECT_ID"The command creates a Plunk draft by default. Review the audience, content, and schedule in Plunk.
If the draft is correct, send it now.
pnpm newsletter:create -- \
--input monthly-newsletter.json \
--project "$GCP_PROJECT_ID" \
--sendIf you need a future send time, add --scheduled-at.
Use an ISO 8601 time and --send.
pnpm newsletter:create -- \
--input monthly-newsletter.json \
--project "$GCP_PROJECT_ID" \
--send \
--scheduled-at 2026-10-01T14:00:00ZGive an agent the JSON field list in this section.
Ask the agent to use confirmed site content and HTTPS SciTeens URLs.
Ask the agent to run the dry command first.
Do not give an agent PLUNK_SECRET_KEY when it only prepares content.
Give send access only to an agent that can create broadcasts.