From an empty machine to the MiStorage API running locally, with a first HQ user signed in and a first location created, in about an hour. Running the web app against it is an optional last step.
Checked against master at d7c422f (backend), main at 5f50dea (web app) on .
| Tool | Version | What it is for |
|---|---|---|
| Git | Any recent one | Cloning from Azure DevOps |
| .NET SDK | 10 (the projects target net10.0) | Building, running and testing the API |
| EF Core CLI | 10.x: dotnet tool install --global dotnet-ef | Creating the database from the migrations |
| PostgreSQL | A local or reachable server. Production runs 17. | The API's only database |
| Node.js and npm | Node 20 or later | Only for the web app, the optional last step |
micampdev Azure DevOps organisation, which holds both repositories.PhoenixGate:Enabled is false) and Development writes outgoing mail to disk.Neither repository has a Dockerfile or a Compose file, so PostgreSQL has to be installed on your machine or reachable from it. Connect as a user that may create databases, so EF can create mistorage for you.
Two repositories in the MiStorage Azure DevOps project: the API and the Next.js web app.
git clone -b master "https://micampdev.visualstudio.com/MiStorage/_git/MiStorage%20Backend" mistorage-backend
git clone "https://micampdev.visualstudio.com/MiStorage/_git/MiStorage"In Azure DevOps the backend's default branch, main, holds only a template README and shares no history with the code. The code and its history are on master: branch from there. The web app's main is the real branch, and AWS Amplify deploys every push to it, so do your work on a branch.
| Path | What is in it |
|---|---|
mistorage-backend/ | The API project: Program.cs, controllers, services, DTOs, entities, Database/AppDbContext.cs, Migrations/, middleware and the appsettings files |
tests/mistorage-backend.Tests/ | The xUnit suite, on EF's InMemory provider |
docs/ | The production migration procedure, the single-operator model and one write-up per working session |
Web app app/, lib/, stores/ | Routes (/login and /super-admin for HQ, /tenant for staff, /s/{subdomain} for the storefront, the api/*-auth relay), API clients and Zustand stores |
Two values are required before the API will start. In Development they come from user-secrets, and the project already carries a UserSecretsId.
cd mistorage-backend
openssl rand -base64 48 # one way to make a signing key: 32 bytes or more
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Database=mistorage;Username=postgres;Password=<your-password>"
dotnet user-secrets set "Jwt:SecretKey" "<the key you generated>"
dotnet user-secrets list| Key | For a first run | Notes |
|---|---|---|
ConnectionStrings:DefaultConnection | Required | Your local PostgreSQL. The API stops at startup without it. |
Jwt:SecretKey | Required | At least 32 bytes. The API stops at startup if it is missing or shorter. |
Security:FieldEncryptionKey | Optional | Only needed to store a renter's Social Security Number. Without it those registrations are refused and everything else works. |
Email:Transport | Already Mock | Set in appsettings.Development.json: mail is written as .html files to mistorage-backend/mail-drop/ instead of being sent. No SMTP credentials needed. |
PhoenixGate:Enabled | Already false | No card is charged, so no gateway credentials are needed. PhoenixGate:BaseUrl must stay set (the tracked files set it): startup builds HTTP clients from it. |
Any key can also come from an environment variable, with __ in place of each colon (ConnectionStrings__DefaultConnection, Jwt__SecretKey). Environment variables win over user-secrets.
Its connection string is production's. appsettings.Development.json blanks it, which is why Development needs your user-secret. Keep the environment at Development (the launch profiles and dotnet ef both default to it): in any other environment user-secrets are not read, and the production value is what is left.
dotnet ef database update # create the database and apply every migration
dotnet ef migrations list # all 19 listed, none marked (Pending)dotnet ef runs the app's startup checks to find the connection, so set the two secrets first. It runs as Development, reads your user-secret, and creates mistorage on your server if it does not exist yet.InitialPostgres to AddApplicationFieldPolicy. They seed three HQ roles (SuperAdmin, Admin, Manager), their permissions, the default system settings and the archived subscription plans.dotnet ef database update again.dotnet run # http profile: http://localhost:5179
dotnet run --launch-profile https # adds https://localhost:7206| Address | What it is |
|---|---|
http://localhost:5179 | The API. Both launch profiles set ASPNETCORE_ENVIRONMENT to Development. |
/scalar/v1 | Scalar, the interactive API reference. Development only. |
/openapi/v1.json | The OpenAPI document behind it |
/api/health | 200 with database reported Healthy once PostgreSQL answers, 503 when it does not |
Startup checks its configuration in one pass and stops with a list of everything that is missing (see Troubleshooting below). The payment workers log that they are idle, because the gateway is switched off.
Create the first HQ user, sign in, call a protected endpoint, then create a location and find it on the public storefront.
API=http://localhost:5179
# 1. Is the API up, and can it reach the database?
curl -s $API/api/health
# 2. Register the first HQ user. The first account on an empty database becomes SuperAdmin.
curl -s -X POST $API/api/auth/register -H "Content-Type: application/json" \
-d '{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","password":"<your-password>","confirmPassword":"<your-password>"}'
# 3. Sign in, then copy data.accessToken from the response
curl -s -X POST $API/api/auth/login -H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"<your-password>"}'
TOKEN=<paste data.accessToken here>
# 4. A protected endpoint (SystemUser policy): who am I?
curl -s $API/api/auth/me -H "Authorization: Bearer $TOKEN"
# 5. Create a location, then read its storefront without a token
curl -s -X POST $API/api/customer -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"companyName":"Demo Storage","contactFirstName":"Ada","contactLastName":"Lovelace","contactEmail":"ada@example.com","subdomain":"demo"}'
curl -s $API/api/public/demo{
"success": true,
"message": "Login successful.",
"data": {
"accessToken": "<jwt>",
"accessTokenExpiration": "2026-10-27T15:30:35Z",
"user": {
"id": "<guid>",
"email": "ada@example.com",
"role": "SuperAdmin",
"permissions": ["Dashboard:View", "Users:View", "..."]
}
},
"errors": null
}confirmPassword must match. Anything else answers 400 with an error per field.mistorage-refresh-token cookie, which curl drops unless you keep a cookie jar, so simply sign in again when the token runs out.Customer is the API's name for a location. The new one is Active at once and gets an Owner login for its contact email. api/public/demo then returns its profile and size guide, with no units yet.http://localhost:5179/scalar/v1 and send the same requests there, with the token as a bearer token for the protected ones.POST api/auth/register is anonymous, and every account after the first becomes a Manager. Locally that is how you bootstrap. On a shared environment it is the known gap tracked in the configuration checklist in docs/single-operator-model.md.
Optional. The Next.js app uses your local API once two variables point at it.
NEXT_PUBLIC_API_URL=http://localhost:5179
API_URL=http://localhost:5179npm install
npm run dev # http://localhost:3000Open http://localhost:3000/login and sign in as the HQ user you registered. The location you created is under Locations, and Manage Facility on its page opens that location's console. Its storefront is at /s/demo.
Without NEXT_PUBLIC_API_URL the HQ screens run on mock data. In a production build (npm run build, then npm run start) sign-in goes through the server-side relay, which falls back to the hosted production API when neither variable is set. NEXT_PUBLIC_* values are baked in at build time, so rebuild after changing them. The API's CORS policy allows localhost:3000 and no other local port.
dotnet test # backend: 249 xUnit tests, no database needed
npx tsc --noEmit # web app: type check
npm run lint # web app: eslint
npm run build # web app: production buildtsc, eslint and next build are the whole safety net. At 5f50dea the type check is clean and lint reports 51 problems (8 errors), all of them older than this guide.RentalTests.A_client_supplied_date_is_stamped_UTC_before_it_is_stored expects 15 October of next year to be more than a year away. From 15 October to 31 December it is not, so the run reports 248 passed and 1 failed. That is the test's calendar, not your setup.
| What you see | Why, and what to do |
|---|---|
The backend clone holds only README.md | You cloned the placeholder main. Run git checkout master. |
Startup configuration is incomplete - 2 problem(s) found | A required key is missing or too short. The message lists every gap at once; set them with dotnet user-secrets set. |
dotnet ef says Unable to create a 'DbContext' of type 'AppDbContext' | The same configuration check failed while EF started the app. The line above it names the key. |
MSB3027, mistorage-backend.exe is locked | A running API locks its build output, and dotnet ef builds first. Stop the API. |
/api/health answers 503 with database Unhealthy | PostgreSQL is not running, or the connection string's host, database or password is wrong. |
| 401 with an empty body | No token, a malformed one, or one past its 15 minutes (the response then carries X-Token-Expired: true). Sign in again. |
| 403 on an endpoint that exists | The token is for another audience. HQ, location staff and renter tokens are not interchangeable. |
400 titled One or more validation errors occurred. | Model validation. It answers in ASP.NET's problem-details shape, not the ApiResponse envelope; errors names each field. |
| 429 Too Many Requests | More than 10 sign-in or registration calls a minute from one address. Wait a minute. |
404 on api/subscription-plans or api/invoice | Archived SaaS endpoints answer 404 while Saas:Enabled is false. |
| A password-reset or verification email never arrives | Development never sends mail. Open the newest .html file in mistorage-backend/mail-drop/. |
Restore warns NU1902 and NU1903 about System.Security.Cryptography.Pkcs 9.0.4 | A known advisory on an indirect dependency of the API (through Microsoft.EntityFrameworkCore.SqlServer). Build and tests still succeed. |