Social Income is a radically simple solution in the fight against poverty. The open-source initiative converts donations into an unconditional basic income, sent directly to the mobile phones of people living in poverty in the Global South.
Social.Income.explained.mp4
This repository contains the public website, internal tools, the shared design system, local development seed data, and the recipient mobile app.
/
├─ design-system/ Shared React components and Storybook
├─ recipients_app/ Mobile app for Social Income recipients
├─ seed/ Firebase emulator seed data
└─ website/ Next.js app, APIs, database, and tests
website/ and design-system/ are npm workspaces. They share one
lockfile at the repository root.
The main Next.js application. It contains:
- Public website: the public Social Income website. Parts are still hardcoded, while more content is being moved to Storyblok CMS.
- Portal: internal operations tool for program management, payments, recipients, contributors, and admin functionality.
- Dashboard: contributor self-service area for payments, subscriptions, and personal details.
- Partner Space: local partner self-service area for recipients, candidates, and partner profile data.
- API routes: backend endpoints used by the website and the recipient mobile app.
- Database layer: Prisma ORM with PostgreSQL.
- Backend: a modular monolith under
website/src/modules. Pages and route handlers call module services and actions. Services call repositories (Prisma) and integrations (external APIs). The module contract is inwebsite/AGENTS.md. - Hosting: Vercel. Settings and cron jobs are in
website/vercel.json. The PostgreSQL database runs on Neon. Firebase stays for auth and storage. - Tests: unit tests and Playwright end-to-end tests.
Shared UI components (@socialincome/design-system): buttons, dialogs,
forms, badges, and other primitives built with Radix and Tailwind. It also
holds the global styles and fonts. The website imports the package, but the
package never imports the website, so other Next.js apps can reuse it.
The components are documented in Storybook. Storybook is deployed as its own
Vercel project with design-system as the root directory. See
design-system/AGENTS.md for the conventions.
Mobile app for recipients. Recipients can log in, view payment history, and
complete surveys. See recipients_app/README.md for mobile setup details.
Seed data for the local Firebase emulators. Firebase Auth users are imported automatically when the local development environment starts.
The conventions for each workspace live in AGENTS.md files, written for
people and coding agents alike:
AGENTS.md: repository overview and the checks to runwebsite/AGENTS.md: backend modules, integrations andlib, and how the website uses the design systemdesign-system/AGENTS.md: what belongs in the design system, component conventions, and design tokens
Most of these rules are enforced by ESLint, so npm run lint tells you
when code does not fit.
Install these tools before starting:
- mise
- Docker
- Node.js and npm through mise
On macOS, install mise with:
brew install misecd website
mise install
npm ci --prefix ..Dependencies are installed once from the repository root for both
website/ and design-system/.
Copy the local env template:
cd website
cp .env.local.sample .env.localFor most external contributors, the only required CMS value is:
STORYBLOK_PREVIEW_TOKEN="<public-content-delivery-api-token>"Despite the name, STORYBLOK_PREVIEW_TOKEN is used by the website to load
Storyblok content through the Content Delivery API. A public token is enough
for frontend and UI work against published content.
If you need this token, ask a maintainer or contact
support@socialincome.org. Do not commit real API keys or secrets.
Maintainers may also need these Storyblok values for preview mode, webhooks, campaign submissions, or schema/type generation:
STORYBLOK_PREVIEW_SECRETSTORYBLOK_WEBHOOK_SECRETSTORYBLOK_MANAGEMENT_TOKENSTORYBLOK_PERSONAL_ACCESS_TOKENSTORYBLOK_SPACE_ID
cd website
mise devThis starts:
- PostgreSQL in Docker
- Firebase emulators for Auth, Firestore, and Storage
- Next.js at
http://localhost:3000
To work on shared components, start Storybook at http://localhost:6006:
cd design-system
npm run storybookThe Firebase emulator UI is available at:
http://localhost:4000
Auth users can be inspected at:
http://localhost:4000/auth
Firebase Auth users are imported automatically from seed/auth_export when
the emulator starts. The PostgreSQL database needs to be seeded once manually:
cd website
npm run db:seedThis fills the local database with representative test data from
website/src/lib/database/seed.
To also create database entries for Storyblok campaigns (so campaign pages join CMS content with local donation data), run:
cd website
npm run db:seed:cms-campaigns:applyThis is create-only: it adds missing campaigns matched by Storyblok
portalSlug and skips rows that already exist. Use npm run db:seed:cms-campaigns:apply:all
to include unlisted campaigns, or npm run db:seed:cms-campaigns for a dry-run.
Requires STORYBLOK_PREVIEW_TOKEN in .env.local (see .env.local.sample).
Stripe cannot call localhost directly. To test payment webhooks locally, install the Stripe CLI and forward events to the website:
brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:3000/api/v1/stripe/webhook \
--events charge.succeeded,charge.updated,charge.failed,customer.updated,customer.subscription.created,customer.subscription.updated,customer.subscription.deletedCopy the webhook signing secret printed by the CLI into website/.env.local:
STRIPE_WEBHOOK_SECRET=whsec_...Restart mise dev, then make a test contribution. The CLI forwards those events to the local server.
The production Stripe webhook endpoint needs the same events: charge.succeeded, charge.updated, charge.failed, customer.updated, customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted.
Open the website at:
http://localhost:3000
Click Login in the top navigation and enter one of these local test users:
| Area | Purpose | |
|---|---|---|
| Portal | Internal operations and admin tool | power@portal.test |
| Dashboard | Contributor self-service area | coreh@dashboard.test |
| Partner Space | Local partner self-service area | sl@partner.test |
In staging and production, login sends a magic link by email. Locally, the
Firebase emulator logs the magic link instead. Copy it from the terminal
running mise dev, or open:
http://localhost:4000/logs
The main integration branch for active development is main.
- Create your feature branch from
main. - Keep your changes focused on one issue or feature.
- Run the relevant checks locally.
- Open a pull request back into
main. - Wait for CI and review.
Example:
git checkout main
git pull
git checkout -b fix/issue-2064-short-descriptionWebsite and design system checks run for pull requests and for pushes to
main.
Vercel deploys through its Git integration: main goes to the staging
environment, the production branch to production, and pull requests get
preview deployments. To release, publish a
GitHub release
with a new tag on main. The Release workflow then pushes that commit
to production, which deploys the website and the Firebase rules.
Vercel runs npm run build, which applies the Prisma migrations before
next build, so a failed migration fails the deployment and the previous
one stays live. The previous deployment keeps serving against the migrated
schema until the new one is live, so migrations must stay backwards
compatible. Previews migrate their own Neon database branch.
The cron jobs in website/vercel.json only run on production. To run one
on staging, call it with Authorization: Bearer $CRON_SECRET.
Production releases are handled by maintainers.
Useful local checks for website changes:
cd website
npm run lint
npm run typecheck
npm run test:unit
npm run test:e2eUseful local checks for design system changes:
cd design-system
npm run lint
npm run typecheck
npm run test:unitIn CI, the E2E job reads Stripe test keys and tokens from the E2E_*
repository secrets (see .github/workflows/website.yml).
For many small UI or content changes, lint and typecheck are a good
minimum before opening a PR. Run the broader test suite when touching shared
logic, authentication, database behavior, or user flows.
We use Storyblok as CMS for parts of the public website.
For normal local development, set the public Content Delivery API token in
website/.env.local:
STORYBLOK_PREVIEW_TOKEN="<public-content-delivery-api-token>"Use local HTTPS if you are working with Storyblok live preview:
cd website
mise run dev-sslIf you changed the Storyblok schema, regenerate the generated TypeScript types. This requires maintainer-level Storyblok credentials:
cd website
npm run storyblok:generateThe command logs into Storyblok, pulls component schemas, and writes generated
types to website/src/generated/storyblok/types.
Campaign submissions and other Management API calls require a Personal Access
Token (PAT) with write access. This is separate from
STORYBLOK_PERSONAL_ACCESS_TOKEN, which the Storyblok CLI uses for schema pull
and type generation.
To create or rotate the token:
- Log in to Storyblok with
dev@socialincome.org. - Open Account Settings → Personal Access Tokens (PAT).
- Create a token named Campaign management token with these scopes:
- Assets: read, write
- Stories: read, write, publish
- Asset folders: read
- Spaces: read
- Set the token lifetime to 1 year.
- Copy the token immediately. Storyblok only shows it once.
Store the token in these places:
-
Local development:
website/.env.localSTORYBLOK_MANAGEMENT_TOKEN="<token>" -
1Password: Social Income maintainer vault (for team access and rotation).
-
Vercel (staging and production): project environment variable
STORYBLOK_MANAGEMENT_TOKEN.
Set a calendar reminder to rotate the token before it expires.
Visitors can submit campaigns from the public /campaigns page. Submissions:
- create an inactive, non-public database
Campaignwith a server-generated slug - upload a primary image and create an unpublished Storyblok
Campaignstory - link database and CMS entries through
Campaign.slug↔Storyblok.content.portalSlug
Publication happens manually in Storyblok. Published Storyblok stories are the sole public visibility gate for campaign pages (detail load and overview join).
Public active vs inactive is derived from campaign end date and goal
progress (not the database isActive flag): a campaign is inactive when its
finish date has passed or its goal amount has been reached. The overview filter
and card linkability use that derived state; deep links to published stories
still work after a campaign becomes inactive.
Server-only configuration lives in
website/src/lib/campaign-submission.ts. Local development and
deployed environments need STORYBLOK_MANAGEMENT_TOKEN; see
Storyblok Management Token for creation and
storage.
The token must be able to list assets in the default-images folder, create
draft stories under pages/campaigns, and upload assets in the configured
asset folder. If a submission fails after partial progress, the API attempts
compensating cleanup of the created Storyblok asset,
Storyblok story, and database row.
Future hardening (not part of the first version): Cloudflare Turnstile and
distributed rate limiting on POST /api/campaign-submissions.
The recipients_app communicates with the Next.js API routes. The public API
documentation is available at:
https://socialincome.org/v1/api-docs
The website pages Slack (#social-income-monitoring) for production
failures that must not stay silent:
sendSlackAlertfromwebsite/src/lib/utils/slack-alert.ts(Stripe webhooks, payment imports, scheduler jobs). It logs the message with aSLACK_ALERTprefix and, whenSLACK_ALERT_WEBHOOK_URLis set, posts it to Slack after the response, at most once every 5 minutes per function instance. Only the message goes to Slack; details stay in the logs. Staging leaves the webhook unset and does not page Slack, because its Stripe and campaign data is incomplete.- Health endpoints for uptime monitoring:
/api/health/website,/api/health/database, and the public homepage/en/int. - Cron runs and function errors are visible in the Vercel dashboard (Logs and Observability).
The recipients app still reports errors to Sentry.
rm -rf website/.next
cd website
mise devThe Firebase emulators load seed data from seed/. If you changed the seed
data and want a fresh start:
docker compose -f website/docker-compose.yml down --remove-orphans --volumes
cd website
mise devIf Prisma migrations fail, old containers are hanging around, or the local DB is in a strange state, reset the website Docker environment:
docker compose -f website/docker-compose.yml down --remove-orphans --volumesThis removes the website Docker containers and named volumes, including local
PostgreSQL data. Run mise dev and npm run db:seed again afterwards.
The Playwright CI job updates changed screenshots and commits them back into
the PR as the socialincome-ci GitHub App, which runs the checks again.
Pull the branch before you push again. For Renovate, Dependabot and fork PRs
the job only compares screenshots and fails if they changed.
cd website
npm run db:seed
npm run db:seed:cms-campaigns:apply
npm run db:studio
npm run db:migrate:devpg_dump -Fc --no-owner "postgresql://social-income:social-income@localhost:5432/social-income" > local.dumppg_restore --clean --if-exists --no-owner -d "<database-url>" local.dumpBecome a contributor of Social Income. Donations are tax-deductible in Switzerland.
Become a sponsor and help build open-source software for more equality and less poverty. Donations through the GitHub Sponsor program support the developer community.
Social Income is a non-profit association (CHE-289.611.695) based in Zurich, Switzerland. Connect with us on X, Instagram, LinkedIn, Facebook, or by email.
We believe that transparency builds trust and trust builds solidarity. This is why we disclose our finances to the public.
Open source is made by people like you. These individuals, among many others, have contributed to Social Income:
We receive in-kind donations from Anthropic, Google Nonprofit, GitHub, Codemagic, Cloudflare, Linktree, Twilio, Algolia, JetBrains, Storyblok, 1Password, Mux, Sentry, Make, Lineto. Our tools also use open-source technologies such as Storybook and Tailwind CSS.
This project is licensed under MIT, with the exception of the Unica77 font, which is exclusively licensed to Social Income.
