Platform
Learn
Developer docs User guide Quickstart Blog
Company
Services About Contact Links Get started

Local development

Updated

One script starts everything Raytha needs for development: PostgreSQL, a mail catcher, S3 and Azure storage emulators, the .NET app with hot reload, and the admin app's dev server. This page covers that script, the ports it uses, the tests, the local CI run and the rules for bumping the version.

What you need

  • .NET SDK 10. global.json pins 10.0.100 and allows newer feature bands.
  • Docker with the Compose plugin, for the dev services.
  • Node.js 24 and pnpm 11 for the admin app. The repository's packageManager field names pnpm 11.9.0, so corepack enable picks the right version.
  • Python 3, for the scripts in tools/. They use only the standard library.

Start it

git clone https://github.com/RaythaHQ/raytha.git
cd raytha
./tools/dev.sh

Open http://localhost:5200. The admin is at /raytha. On a fresh database you land on the setup screen; see the Quickstart.

tools/dev.sh does three things:

  1. Runs tools/compose-up.sh, which starts the services in tools/compose.yaml (Compose project raytha-dev). It skips any service whose published host port is already taken by something else and says so, instead of failing. Set SKIP_COMPOSE=1 to skip this step entirely when you run your own PostgreSQL.
  2. Exports the development settings in the table below, each only if you have not already set it.
  3. Runs dotnet watch --project src/Raytha.Web, which rebuilds and restarts on C# changes.
VariableValue set by dev.sh
ASPNETCORE_ENVIRONMENTDevelopment
ASPNETCORE_URLShttp://0.0.0.0:5200
ConnectionStrings__DefaultConnectionHost=localhost;Port=5433;Username=postgres;Password=changeme;Database=raytha
APPLY_PENDING_MIGRATIONStrue
SMTP_HOST, SMTP_PORTlocalhost, 1025 (the mail catcher)
AdminSpa__DevServerUrlhttp://localhost:5203
AdminSpa__AutoStarttrue

Use the other variables from Configuration the same way: export them before running the script.

Ports and services

PortWhatNotes
5200Raytha (public site, admin, APIs)Bound to 0.0.0.0, so another device on your network can reach it.
5203Admin dev server (Vite)Started and proxied by the .NET app.
5433PostgreSQL 17User postgres, password changeme, database raytha.
1025, 8025MailHog SMTP, MailHog web UIRead caught mail at http://localhost:8025.
9000, 9001MinIO S3 API, consoleUser raytha, password DevPassw0rd!. Create a bucket in the console before using it.
10000Azurite (Azure Blob emulator)Use the well-known development account connection string.

To try cloud storage locally, set FILE_STORAGE_PROVIDER=S3 with FILE_STORAGE_S3_SERVICE_URL=http://localhost:9000 and the MinIO credentials, or AzureBlob with an Azurite connection string. Details in File storage.

Warning The release docker-compose.yml also publishes PostgreSQL on host port 5433. Run one or the other. If compose-up.sh says port 5433 is in use and skips PostgreSQL, the app will connect to whatever else is listening there.

Demo data

tools/seed.py builds a full demo site through the admin API, so the data is what the admin app itself would write. It creates content types covering every field type, a few hundred items in mixed states, views, media, users, roles, functions, templates, site pages, menus and webhooks. Use a fresh database:

python3 tools/seed.py --base-url http://localhost:5200 \
  --email [email protected] --password 'your-password' --setup

--setup runs first-run setup with those credentials. Without it, the script signs in with them.

Work on the admin app

The admin is a pnpm workspace in src/admin:

  • apps/shell (@raytha/shell): the application, built on TanStack Router.
  • packages/api (@raytha/api): the typed client for /raytha/api.
  • packages/ui (@raytha/ui): shared components.

With dev.sh, the .NET app starts Vite on port 5203 itself and runs pnpm install first if node_modules is missing. If pnpm is not on your PATH it logs that and serves the published bundle instead. To run Vite yourself, set AdminSpa__AutoStart=false and:

cd src/admin
pnpm install
pnpm dev

Visit the app on port 5200, not 5203: the .NET app proxies the admin's paths to Vite and owns everything else, such as the API and uploads. The list of server-owned paths is in AdminSpaExtensions.ServerPathPrefixes and is mirrored in the Vite proxy configuration. The list covers /raytha/api, /raytha/media-items, the SSO, JWT and SAML login callbacks, /raytha/logout and a few more. Add a new server-handled route under /raytha to both places.

cd src/admin
pnpm lint
pnpm test
pnpm typecheck
pnpm build

pnpm build writes the production bundle to src/Raytha.Web/wwwroot/raytha. That output is committed. Commit the rebuilt bundle with your source change, because CI rebuilds it and fails if the committed files differ.

Run the tests

dotnet test Raytha.sln

The solution has four NUnit 4 projects, using FluentAssertions and Moq:

  • Raytha.Domain.UnitTests, Raytha.Application.UnitTests, Raytha.Infrastructure.UnitTests
  • Raytha.Architecture.Tests: the layer, CQRS, authorization and proxy rules described in Architecture.

Tests must not use a real PostgreSQL, network or SMTP server. Run one project with dotnet test tests/Raytha.Architecture.Tests.

Run CI locally

./tools/ci-local.sh

This is a local stand-in for .github/workflows/ci.yml, and you should run it before you push. In order, it:

  1. Checks VERSION against origin/dev (skipped if there is no origin/dev).
  2. Restores, builds in Release and runs all tests.
  3. Runs tools/check-sql-scripts.py, which regenerates db/Postgres/FreshCreateOnLatestVersion.sql from the migrations and fails if it differs from the committed file.
  4. In src/admin: installs with a frozen lockfile, then lint, test, typecheck and build, and fails if the committed bundle changed.
  5. Audits NuGet packages for known vulnerabilities.

The CI workflow runs the same version, backend and admin jobs for pull requests to main and dev. On pushes it also builds the Docker image and scans it with Trivy for critical and high vulnerabilities. Tagged releases are published to Docker Hub as raythahq/raytha:On pushes it also builds the Docker image and scans it with Trivy for critical and high vulnerabilities. It does not publish the image.

lt;versionOn pushes it also builds the Docker image and scans it with Trivy for critical and high vulnerabilities. It does not publish the image.

gt;
and latest.

Versioning rules

The VERSION file holds MAJOR.MINOR.PATCH and is the product version: the build reads it into every assembly, and /healthz reports it. CI requires it to describe the code in the same commit.

  • A change that adds a new EF migration file under **/Persistence/Migrations/*.cs bumps MINOR and resets PATCH. Designer and snapshot files do not count.
  • Every other releasable change bumps PATCH.
  • MAJOR is a human decision. The scripts never change it.
  • Commit the new VERSION with the change it describes.
python3 tools/bump-version.py --kind patch --write   # or --kind minor
python3 tools/check-version.py --before origin/dev --after HEAD

If check-version.py expects a different number than you wrote, fix VERSION, not the script. A maintainer can accept an unusual version, such as a deliberate skip, by adding the version-override label to the pull request, with the reason in the description. Locally, VERSION_OVERRIDE=1 ./tools/ci-local.sh does the same.

Name a migration after the release that ships it, not after what it changes: v2_1_0, not AddCoolThing.

dotnet ef migrations add v2_1_0 \
  --project src/Raytha.Infrastructure \
  --startup-project src/Raytha.Web

A schema change must update db/Postgres in the same commit, because operators who cannot run migrations at startup apply these scripts by hand. Generate the upgrade script from the previous release's migration to the new one (new releases get a new v<previous>_to_v<new>.sql; older scripts are frozen), and refresh FreshCreateOnLatestVersion.sql the same way:

dotnet ef migrations script v2_0_0 v2_1_0 \
  --project src/Raytha.Infrastructure \
  --startup-project src/Raytha.Web \
  --output db/Postgres/v2_0_0_to_v2_1_0.sql

tools/check-sql-scripts.py regenerates both files and fails if either is missing or differs from what is committed. It ignores only the EF version stamp and the default-theme seed row, which change on every generation. If it fails, its output prints the exact dotnet ef migrations script commands to run.

Gotchas

  • dev.sh uses APPLY_PENDING_MIGRATIONS=true, so a branch with a newer migration changes your dev database when you switch to it. Switching back does not undo it; drop the database (docker compose -f tools/compose.yaml down -v removes all dev volumes) if you need a clean slate.
  • A stale .env in a parent directory of the repository overrides environment variables. See Configuration.
  • A running instance whose reported version differs from VERSION is a stale build. Check /healthz.

Next steps