The Summer EBT (SUN Bucks) Self-Service Portal is an application that allows parents/guardians of children eligible for Summer EBT manage their benefit, including the following core features:
- Verifying a child's eligibility
- Verifying when and how the benefit will be received (which EBT card)
- Changing mailing address on file
- Requesting a replacement EBT card
Backend
- Language/framework: C# with .NET 10
- Key libraries: ASP.NET Core, Serilog, Managed Extensibility Framework (MEF), EntityFramework (EF) Core
- Package manager: NuGet
Frontend
- Language/framework: NextJS 16 with TypeScript
- Key libraries: next, react, i18next, react-i18next, tanstack/react-query, zod
- Package manager: pnpm
- Design system: USWDS, with design tokens specified for each state
Infrastructure
- Infrastructure as Code using OpenTofu (Terraform) - see tofu
- Docker with docker-compose for local development
Note: The following steps assume you are working on macOS. Steps may differ if you are working on a different operating system.
- .NET 10 SDK for running the back end
- The latest version of nodeJS
- pnpm for managing front end packages and development scripts
- Docker Desktop for running and managing containers (includes MSSQL database)
Clone this repository on your local machine, alongside the state connector repository and any revelant state backend connector(s) - for example, Colorado - as siblings (within the same parent folder). Note that you will need to build and set up all repos as part of your local env setup.
git clone git@github.com:codeforamerica/sebt-self-service-portal.git
git clone git@github.com:codeforamerica/sebt-self-service-portal-state-connector.git
# Colorado:
git clone git@github.com:codeforamerica/sebt-self-service-portal-co-connector.git
.env files are used in this project to set environment variables (eg, database configs). This is a preferred pattern for 12-factor Apps. They are also set to fallback to a generic default. You'll need to create .env files for your local environment, based on the example file.
To create your local .env file with configurations for the database and API, run this command in the root of the repo:
cp .env.example .envYou'll want do the same from within /src/SEBT.Portal.Web:
cp .env.example .env.localYou'll also need an API appsettings file for your local machine with certain values set (see state specific configuration below):
cd src/SEBT.Portal.Api
cp appsettings.Development.example.json appsettings.Development.json Front end
- To install all javascript package dependencies, run
pnpm installfrom the root of this repository. - You can learn more about the front end in the SEBT.Portal.Web README
Back end
-
.NET tools are CLI utilities installed and managed using NuGet. Currently, we are using the
nuget-licensetool for auditing backend dependency license. Needed tools are defined in the tools manifest in.config/dotnet-tools.json. To install .NET tools, rundotnet tool restorefrom each solution root (ie, each top-level directory containing a.slnor.slnxfile):- /src/SEBT.Portal.Infrastructure
- /src/SEBT.Portal.Api
-
You'll also want to run
dotnet buildfrom within the root of each repository before starting up the app for the first time.
Make sure Docker is installed and the docker daemon is running. When the database spins up locally, all migrations will be run and db seeded automatically (see database setup section below).
docker compose up -d # Start all docker containers, including MSSQL Database and Mailpit for testingpnpm dev # Script to start both API (ie, `dotnet watch`) and frontend (ie, `next dev`)To open the app, navigate to https://localhost:3000
# Start frontend only
pnpm web:dev
# View logs
docker compose logs -f
# Stop all services
docker compose down
# Stop and remove volumes (clears database - do this only if you're OK with dropping your seeded data)
docker compose down -vMailpit captures all outgoing emails in local development. Once the Mailpit docker container is running on your machine, you can access its UI in your browser at http://localhost:8025
Redis is used as an optional distributed cache backing for HybridCache. It's included in Docker Compose and starts automatically with docker compose up -d.
To enable Redis caching for a state, add a Redis connection string to the state's appsettings.{state}.json:
"ConnectionStrings": {
"Redis": "localhost:6379"
}When no Redis connection string is configured, the application falls back to in-memory caching only. See appsettings.co.example.json for an example.
pnpm api:build # Build backend only (Debug)
pnpm api:test # Test backend onlypnpm ci:build # Build frontend + backend (Release)
pnpm ci:test # Test frontend + backend
# Individual components
pnpm ci:build:frontend # Build frontend only
pnpm ci:build:backend # Build backend only
pnpm ci:test:frontend # Test frontend only
pnpm ci:test:backend # Test backend only# State-based CI testing
pnpm ci:test:states # Test all states
pnpm ci:test:state:dc # Test DC state
pnpm ci:test:state:co # Test CO state
# Utility commands
pnpm ci:list # List all ACT workflows
pnpm ci:validate # Validate workflows (dry-run)State-Specific Development:
deploy/dc-* # DC-only changes (only DC builds in CI)
deploy/co-* # CO-only changes (only CO builds in CI)Shared Development:
feature/* # Changes for all states (all states build in CI)
main # Production source for all statesHow it works: main contains all code (shared + state-specific). Each state deployment uses only what it needs via configuration and feature flags.
See docs/development/state-ci.md for detailed CI documentation.
The API loads state-specific configuration based on the STATE environment variable:
appsettings.json: Base configuration (always loaded)appsettings.{STATE}.json: State overrides (loaded whenSTATEis set)
When STATE is set, the API looks for appsettings.{state}.json in the application directory. Values in the state file override those in appsettings.json if present.
Example: With STATE=dc, the API loads appsettings.dc.json. With STATE=co, it loads appsettings.co.json.
# Build and run for DC (loads appsettings.dc.json (if present))
STATE=dc dotnet run --project src/SEBT.Portal.Api
# Docker Compose uses STATE from .env
docker compose upOnly include sections you want to override; other settings fall back to appsettings.json!
States can use an external OpenID Connect (OIDC) provider for sign-in. OIDC is configured in the API under flat Oidc keys (DiscoveryEndpoint, ClientId, CallbackRedirectUri); the portal uses generic endpoints and config rather than state-specific auth code paths. Code exchange and id_token validation run in the Next.js server; the .NET API performs "complete-login" (validates a short-lived callback token and returns a portal JWT that includes IdP claims such as phone and name).
For a deployment that uses OIDC, in .env.local under SEBT.Portal.Web, set:
OIDC_DISCOVERY_ENDPOINTOIDC_CLIENT_IDOIDC_CLIENT_SECRETOIDC_REDIRECT_URIOIDC_COMPLETE_LOGIN_SIGNING_KEY(at least 32 characters)
In appsettings under SEBT.Portal.Api, set:
Oidc:CompleteLoginSigningKey(same value asOIDC_COMPLETE_LOGIN_SIGNING_KEY)Oidc:DiscoveryEndpointOidc:ClientIdOidc:CallbackRedirectUriOidc:LanguageParam(optional)
The API serves public config via GET /api/auth/oidc/{stateCode}/config (no secrets in that response).
See src/SEBT.Portal.Api/appsettings.Development.example.json and ADR-0008.
For states that use phone number as their primary Household ID and OIDC, local development sometimes requires bypassing MFA. You can override the phone number used for household lookup in appsettings.Development.json.
Only active when ASPNETCORE_ENVIRONMENT=Development. Example:
"DevelopmentPhoneOverride": {
"Phone": "8185558437"
}The resolver then uses this phone for household lookup instead of the one from the JWT or user record. You can still complete the OIDC flow as usual; the phone number used to satisfy MFA may differ from the one the portal uses for lookups.
The IdProofingRequirements config section controls which IAL (Identity Assurance Level) a user needs to view or modify each type of PII. Keys use a resource+action format (e.g. address+view, card+write). Values can be a uniform level ("IAL1plus") or a per-case-type object for granular control. Unconfigured keys default to IAL1plus (fail-safe). Users below the view threshold see masked data (e.g. **** for street addresses); users below the write threshold are blocked from modifications.
See the full configuration guide for all available keys, per-case-type syntax, coherence validation rules, and state-specific examples. See appsettings.dc.example.json and appsettings.co.example.json for working state configurations.
The application uses Microsoft SQL Server as its database. This is propped up via a Docker container for local development.
Configuration is managed through environment variables.
Available environment variables for .env in the respository root:
Database (for Docker Compose):
MSSQL_SA_PASSWORD- SQL Server SA passwordMSSQL_DATABASE- Database nameMSSQL_USER- Database userMSSQL_SERVER- Server hostname (for local)MSSQL_PORT- Server port
API
JWTSETTINGS__SECRETKEY- Secret key for JWT token signing. Must be at least 32 characters.IDENTIFIERHASHER__SECRETKEY- Secret key for HMAC-SHA256 hashing of Household Identifiers as needed. Must be at least 32 characters.
The application uses EF, or Entity Framework Core migrations to manage database schema changes.
Migrations run automatically on application startup. When the API starts, it checks for pending migrations and applies them automatically. This ensures the database schema is always up-to-date.
While migrations run automatically, you can also manage them manually by installing ef on your local machine:
List all migrations:
dotnet ef migrations list \
--project src/SEBT.Portal.Infrastructure/SEBT.Portal.Infrastructure.csproj \
--startup-project src/SEBT.Portal.Api/SEBT.Portal.Api.csprojApply pending migrations:
dotnet ef database update \
--project src/SEBT.Portal.Infrastructure/SEBT.Portal.Infrastructure.csproj \
--startup-project src/SEBT.Portal.Api/SEBT.Portal.Api.csprojCreate a new migration:
dotnet ef migrations add MigrationName \
--project src/SEBT.Portal.Infrastructure/SEBT.Portal.Infrastructure.csproj \
--startup-project src/SEBT.Portal.Api/SEBT.Portal.Api.csprojRemove the last migration (if not applied):
dotnet ef migrations remove \
--project src/SEBT.Portal.Infrastructure/SEBT.Portal.Infrastructure.csproj \
--startup-project src/SEBT.Portal.Api/SEBT.Portal.Api.csprojMigrations are stored in src/SEBT.Portal.Infrastructure/Migrations/:
- Each migration has a timestamp prefix (e.g.,
20251212171249_AddUserOptInTable.cs) - The
PortalDbContextModelSnapshot.csfile tracks the current model state - Migration files should be committed to version control
The database is automatically seeded with test users when running in the Development environment. Seeding occurs automatically during:
- Database migrations (
dotnet ef database update) - Application startup (when migrations are applied)
DbContext.EnsureCreated()calls
The automatic seeding uses EF Core's UseSeeding mechanism under the hood. See https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding
To help test different workflows and users in different states, the seeder will create the following users unless instructed otherwise:
co-loaded@example.com- A co-loaded user with completed ID proofingnon-co-loaded@example.com- A non-co-loaded user with in-progress ID proofingnot-started@example.com- A user who hasn't started ID proofing
Seeding only runs if no users exist in the database, preventing duplicate data on subsequent runs.
There's occasionally going to be instances where you'd want have the auto-seeded data be not be created for certain types of testing. For those instances, there's a small console app to help with this.
To clear all seeded data from the database, use the ClearSeededData console application:
dotnet run --project scripts/ClearSeededDataThis will prompt for confirmation before deleting all seeded records from the database. This is irreversable; once done, you'll have to reseed.
View database tables example:
docker exec -it sebt_mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P YourStrong@Passw0rd -d SebtPortal -C \
-Q "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'"Alternatively, I'd highly recommend a tool like LINQPad to help with DB-related tasks.
More documentation can be found in the docs folder.
See also:
We use Lightweight Architecture Decision Records for tracking architectural decisions, using adr tools to store them in source control. These can be found in the docs/adr folder.