NEXUS
Resources

MiStorage Resources

Repositories, local setup, configuration reference and the safety nets around MiStorage.

mistorage-backendThe API: ASP.NET Core (.NET 10), EF Core, PostgreSQL. Pushed to GitHub and Azure DevOps.GitHubMiStorage (frontend)The Next.js web client: /super-admin, /tenant and the /s/{subdomain} storefront. AWS Amplify rebuilds it from main on every push — deploy the API first, or together.GitHubAzure DevOps projectSecond remote for both repositories under the MiStorage project.Azure DevOps
Test suite249 xUnit tests in tests/mistorage-backend.Tests, on EF's InMemory provider. Run with dotnet test from the repo root.xUnit
Performance reportLoad testing, EXPLAIN ANALYZE and static review, with the fixes applied (SQL-side billing summary, grouped dashboard query, added indexes). Lives in the frontend repo at perf/PERFORMANCE-REPORT.md.Frontend repo
Security hardening65 security tasks tested and passed, raw-PAN paths removed (SAQ A-EP), CSP and page-integrity reporting on the payment pages.See release notes

Run it locally

  1. 1
    Prerequisites.NET 10 SDK, a reachable PostgreSQL, and the EF Core CLI: dotnet tool install --global dotnet-ef.
  2. 2
    Seed your secretsSecrets are deliberately blank in the tracked appsettings. From the mistorage-backend project directory, set at least the connection string and JWT key with dotnet user-secrets set — the project already carries a UserSecretsId.
  3. 3
    Create the schemadotnet ef database update applies the migrations and seeds roles, permissions and default settings. No admin user is seeded — register the first system user through the API or the frontend.
  4. 4
    Rundotnet run serves http://localhost:5179; dotnet run --launch-profile https adds https://localhost:7206. In Development the Scalar reference is at /scalar/v1.
Seeding a development machine
cd mistorage-backend
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Database=mistorage;Username=postgres;Password=<yours>"
dotnet user-secrets set "Jwt:SecretKey" "<at least 32 bytes of randomness>"
dotnet user-secrets list
dotnet ef database update
dotnet run

Configuration reference

Every secret is empty in the tracked files on purpose. Supply them from user-secrets in development and from environment variables or a secret store in production. A blank value is never a usable default.

Secrets
SettingEnvironment variableBehaviour when missing
ConnectionStrings:DefaultConnectionConnectionStrings__DefaultConnectionFails fast at startup
Jwt:SecretKey (≥ 32 bytes)Jwt__SecretKeyFails fast at startup
Email:Username / Email:PasswordEmail__Username / Email__PasswordSMTP send fails at runtime
PhoenixGate:MerchantPasswordPhoenixGate__MerchantPasswordFails fast while PhoenixGate:Enabled is true
PhoenixGate:QuickPaymentsKeyPhoenixGate__QuickPaymentsKeyFails fast while PhoenixGate:Enabled is true
PhoenixGate:ResellerPasswordPhoenixGate__ResellerPasswordError on merchant boarding only
Security:FieldEncryptionKey (32 bytes, base64 or hex)Security__FieldEncryptionKeyRegistrations carrying a Social Security Number are refused; everything else keeps working. Never falls back to plaintext.
Security:TrustedProxySecretSecurity__TrustedProxySecretOptional, but set it on both sides or neither: the frontend's matching variable is TRUSTED_PROXY_SECRET. Unset, the API ignores the relay's X-Client-Ip, so every relayed login — HQ, staff and renter — shares one rate-limit bucket and audit rows name the relay. In production it is set on the API (2026-09-22) but not yet on the frontend.
Behaviour switches
SettingDefaultWhat it does
Saas:Enabledfalsefalse is the single-operator product. true restores the multi-tenant SaaS model whole. Mirror it in the frontend's NEXT_PUBLIC_SAAS_ENABLED.
RenterAuth:RequireEmailVerificationtrueOn: a renter must confirm their email before the account works. Off: registration creates the account already verified and signs the renter in. Currently off in the tracked files while email delivery is blocked.
PhoenixGate:EnabledfalseWhether cards are charged at all. Off, renting still completes and the first invoice is left outstanding for the location to collect.
Email:TransportSmtpMock writes outgoing mail to Email:MockDropPath instead of sending — used while SMTP is unavailable.
Frontend environment variables (MiStorage)
VariableWhat it does
NEXT_PUBLIC_API_URLThe API base for bearer calls from the browser. Unset, the app runs on mock data.
API_URLThe API base the server-side auth relay uses; falls back to NEXT_PUBLIC_API_URL. Needed in production builds.
NEXT_PUBLIC_USE_MOCKtrue forces mock data even with an API URL
NEXT_PUBLIC_SAAS_ENABLEDMirror of the API's Saas:Enabled; baked in at build time
NEXT_PUBLIC_PAYMENT_GATEWAY_ORIGINSWhere the browser may send card data (the payment-page CSP connect-src). Must change when the merchant leaves the gateway sandbox — tokenization fails closed.
NEXT_PUBLIC_SECURITY_REPORT_URLWhere CSP reports go; defaults to the API's /api/security/csp-report
NEXT_PUBLIC_CSP_MODEreport-only observes the baseline CSP without enforcing it. The payment-page policy always enforces.
TRUSTED_PROXY_SECRETShared with the API's Security:TrustedProxySecret so the relay can report the real client address

Shipping to production

There is no CI on the API, and Amplify deploys the frontend on every push to main — so the order is on you.

  1. 1
    Back up the siteApp Service keeps no deployment history for zip or MSDeploy publishes. Pull the current site as a zip into docs/deploy/backups/ and check it opens.
  2. 2
    Migrate, if the change has a migrationFollow docs/deploy/README.md: an idempotent script, audited, run against a backed-up database. Code ahead of its migration breaks, even for an additive one.
  3. 3
    Publish the APIFrom a local Release build (Visual Studio or a zip / MSDeploy profile). Warm the site up first if it has been idle; Always On is enabled.
  4. 4
    Push the frontendMerge to main; Amplify builds and serves it in a few minutes. An API ahead of its frontend is the safe order.
  5. 5
    VerifyGET /api/health on both, then sign in as HQ, as staff and as a renter.

Migrations cheat-sheet

Local (stop the API first — it locks bin/)
dotnet ef migrations add <Name>     # create a migration from model changes
dotnet ef database update           # apply pending migrations (LOCAL only)
dotnet ef migrations remove         # undo the last (unapplied) migration
dotnet ef migrations list           # what is applied, what is pending
Production — generate a script and read it, never apply directly
ASPNETCORE_ENVIRONMENT=Development dotnet ef migrations script --idempotent \
  --project mistorage-backend --output docs/deploy/migrate-idempotent-<yyyyMMdd>.sql

grep -nE "DROP COLUMN|DROP TABLE|RENAME|ALTER COLUMN|DROP CONSTRAINT|NOT NULL" <script>
  • Audit the generated SQL, not the migration's C#. A backfill written as WHERE "Col" = TIMESTAMPTZ '0001-01-01' matched nothing on Postgres, because Npgsql renders DateTime.MinValue as -infinity.
  • Publish and migrate together, every time. Deploying code ahead of its migration makes every query that maps the new column fail with 42703: column does not exist — that took the HQ dashboard down for four days in September.
  • NOT NULL added to an existing table needs a DEFAULT; narrowing a column fails on any row already longer.

Traps that cost real time

  • **The frontend's /api/*-auth/ relay routes handle POST only.** Route a GET or PUT through them and you get 405 in production and success in development.
  • The backend tests use EF's InMemory provider. No ExecuteDelete, no real SQL semantics, no constraint enforcement. Anything involving raw SQL, deletes or types needs a real database to be believed.
  • The frontend has no test framework. tsc, eslint and next build are the whole safety net; changes to auth or checkout are verified by driving a real browser against a real backend.
  • A running API locks its own build output, and dotnet ef builds before it does anything. Stop the API before migrations add, database update or migrations script.
  • The guided tour finds its targets by `data-tour` attributes. Rename or remove one and that step silently becomes a centred card. Sidebar entries get nav-<last URL segment> automatically, so renaming a route renames its target.
  • Manage Facility replaces the browser's staff session. The staff refresh cookie is browser-wide, so entering a location signs that browser out of whatever location it was in.
  • Read a new API field with a default at the client boundary. In the window between the API and frontend deploys, the UI briefly runs against an API that does not send the field yet.
Found a security issue?Report it through the bug bounty program instead of public channels.