Run Locally#

Everything you need to bring up the Cube 2.0 backend on your machine.

Prerequisites#

  • .NET 8 SDK — verify with dotnet --version8.x
  • Docker Desktop — for SQL Server + Seq log viewer
  • Git with submodule support
  • Optional: dotnet-ef global tool for creating migrations
    dotnet tool install -g dotnet-ef
  • Apple Silicon users: the compose stack uses azure-sql-edge (arm64-native). The classic mssql/server image does not run under QEMU — see ADR 0004.

First-time setup#

git clone git@bitbucket.org:MANEXaloha/cube2-backend.git
cd cube2-backend
git submodule update --init --recursive        # pulls the docs theme

cp .env.example .env                            # docker-compose vars
cp src/Manex.Web/appsettings.Development.json.example \
   src/Manex.Web/appsettings.Development.json   # local secrets (gitignored)

Edit src/Manex.Web/appsettings.Development.json:

  • Set Jwt:SigningKey — any string ≥ 32 chars for dev.
  • Set ConnectionStrings:eManEx if you’re using Mode B (see below); leave it if you’re using Mode A and picking values from .env.

Pick a run mode#

There are two supported modes for local dev. They differ only in which database the API talks to.

Mode A — fresh sandbox#

Empty azure-sql-edge container. No real data. Best for isolated integration work, running the test suite locally with a real SQL provider, or trying auth flows without production risk.

docker compose -f deploy/docker/docker-compose.yml up -d db seq
dotnet run --project src/Manex.Web
  • API on http://localhost:5000
  • Seq (Serilog log viewer) on http://localhost:5341
  • SQL Server on localhost:1433 (sa / YourStrong@Passw0rd from .env)

The sandbox starts empty. No BcomMX_dev database, no aspnet_Users, no seed data. Login will 500 until you seed something. This is intentional — ADR 0004 says the API never creates schema against a real DB, so the sandbox mirrors that.

To make login work locally, seed a minimal user. Save this as seed-dev.sql:

IF DB_ID('BcomMX_dev') IS NULL CREATE DATABASE BcomMX_dev;
GO
USE BcomMX_dev;
GO
CREATE TABLE dbo.aspnet_Users (
    UserId uniqueidentifier PRIMARY KEY,
    ApplicationId uniqueidentifier NOT NULL,
    UserName nvarchar(256), LoweredUserName nvarchar(256),
    MobileAlias nvarchar(16), IsAnonymous bit NOT NULL,
    LastActivityDate datetime2
);
CREATE TABLE dbo.aspnet_Membership (
    UserId uniqueidentifier PRIMARY KEY,
    ApplicationId uniqueidentifier NOT NULL,
    Password nvarchar(max), PasswordFormat int NOT NULL DEFAULT 0,
    PasswordSalt nvarchar(max), Email nvarchar(256), LoweredEmail nvarchar(256),
    IsApproved bit NOT NULL DEFAULT 1, IsLockedOut bit NOT NULL DEFAULT 0,
    CreateDate datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    LastLoginDate datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    LastPasswordChangedDate datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    LastLockoutDate datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    FailedPasswordAttemptCount int NOT NULL DEFAULT 0,
    FailedPasswordAttemptWindowStart datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    FailedPasswordAnswerAttemptCount int NOT NULL DEFAULT 0,
    FailedPasswordAnswerAttemptWindowStart datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    Comment nvarchar(max)
);
GO
INSERT INTO dbo.aspnet_Users (UserId, ApplicationId, UserName, LoweredUserName, IsAnonymous)
VALUES ('11111111-1111-1111-1111-111111111111',
        '22222222-2222-2222-2222-222222222222',
        N'admin', N'admin', 0);
-- BCrypt cost-11 hash of literal password: hunter2
INSERT INTO dbo.aspnet_Membership (UserId, ApplicationId, Password)
VALUES ('11111111-1111-1111-1111-111111111111',
        '22222222-2222-2222-2222-222222222222',
        N'$2a$11$3V2BgB0hIVWeiDgzwhNg9u14cmUVBDkgr/ZNwLldQ8GMP671HlipG');
GO

Run it against the sandbox:

docker run --rm --network cube2-backend_app-network \
    -v "$PWD/seed-dev.sql:/seed.sql" \
    mcr.microsoft.com/mssql-tools \
    /opt/mssql-tools/bin/sqlcmd -S db -U sa -P 'YourStrong@Passw0rd' -i /seed.sql

You can now POST /api/v1/auth/login with admin / hunter2. A repeatable deploy/sql/seed-dev.sql script is Phase 1 follow-up work.

Mode B — against the real legacy DB#

Talks to the actual BcomMX_01_04 on 192.168.9.111\SQLDEV2017 (or your personal restore of a backup). Best for feature work that needs real data.

Edit .env and set the real connection string:

ConnectionStrings__eManEx=Server=192.168.9.111\SQLDEV2017;Database=BcomMX_01_04;User Id=<your-user>;Password=<your-pass>;TrustServerCertificate=True;Encrypt=False;

Then bring up the API + Seq, no db container:

docker compose \
    -f deploy/docker/docker-compose.yml \
    -f deploy/docker/docker-compose.legacy.yml \
    up -d web seq
  • API on http://localhost:8080 (in-container)
  • Seq on http://localhost:5341

The API never runs migrations against the legacy DB. It reads and writes with EF Core against the existing schema. Any future schema change goes through a forward-only migration authored against a scaffolded snapshot — see ADR 0004.

Verify it’s up#

curl -sS http://localhost:5000/health
# → {"status":"Healthy",…}

curl -sS http://localhost:5000/ready
# → {"status":"Healthy",…"sqlserver":{"status":"Healthy"}…}   ← Mode A + seeded, or Mode B

Open http://localhost:5000/docs for Swagger UI.

Get a JWT#

curl -X POST http://localhost:5000/api/v1/auth/login \
    -H 'Content-Type: application/json' \
    -d '{"username":"admin","password":"hunter2"}'

Response (abbreviated):

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiI...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": { "userId": "…", "username": "admin", "roles": [], "permissions": [] }
  }
}

Roles and permissions are empty on Phase 0 — group → claim mapping ships in Phase 1.

Use the token:

TOKEN=eyJhbGci...
curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/v1/auth/me
curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/v1/auth/permissions

Or paste the token into Swagger’s Authorize dialog and hit protected endpoints from the UI.

Run tests#

dotnet test cube2-backend.sln                     # everything (9 smoke tests)
dotnet test tests/Manex.Web.Tests                 # unit
dotnet test tests/Manex.Data.Tests                # EF InMemory
dotnet test tests/Manex.IntegrationTests          # WebApplicationFactory

Integration tests spin up the full API in-process with an in-memory DbContext — they don’t need Docker.

Add an EF migration#

dotnet ef migrations add <Name> \
    --project src/Manex.Data \
    --startup-project src/Manex.Web \
    --output-dir Migrations

Read ADR 0004 first. The first real migration must be authored against a scaffolded snapshot that already includes the legacy schema — otherwise its Up() will try to CREATE TABLE for things that already exist in BcomMX.

Common issues#

  • Jwt:SigningKey must be set (min 32 chars) on startup — your appsettings.Development.json is missing the key, or the env var Jwt__SigningKey isn’t exported.
  • ConnectionStrings:eManEx is missing — same story with the connection string.
  • /ready returns 503 with Cannot open database "BcomMX_dev" — Mode A, sandbox is empty. Run the seed script above.
  • /ready returns 503 with Login failed for user 'sa' — password mismatch between .env SA_PASSWORD and the value embedded in ConnectionStrings__eManEx.
  • Globalization Invariant Mode is not supported — you’re on an older commit; pull latest, this was fixed in 21a6ddc.
  • Docker build can’t find Manex.Contracts.csproj — you’re building from deploy/docker/; the build context must be the repo root.
  • Empty docs/themes/hugo-book on a fresh clone → forgot the submodule step. Run git submodule update --init --recursive.
  • Migrations .cs files empty on fresh clone — expected. Phase 0 ships no migrations (database-first).

Shut it all down#

docker compose -f deploy/docker/docker-compose.yml down -v   # -v also drops the sandbox DB volume
# or, for Mode B:
docker compose \
    -f deploy/docker/docker-compose.yml \
    -f deploy/docker/docker-compose.legacy.yml \
    down

If you hit Ctrl-C on dotnet run, the API stops immediately — no cleanup needed.