MiStorage is a self-storage facility management platform: an HQ console for the operator, a day-to-day console for each location's staff, and a public storefront where renters browse units, sign up and pay. The backend is a REST API built on ASP.NET Core (.NET 10), Entity Framework Core and PostgreSQL, with JWT authentication.
Three audiences, three token types, three front doors. Every request is scoped to one of them.
Renter accounts are scoped per location: the same person legitimately holds separate accounts at two locations, so the unique key is CustomerId + Email, not Email alone.
MiStorage ships in two shapes, and one setting decides which.
It was built as a multi-tenant SaaS platform selling subscriptions to storage companies. Since 2026-09-10 that is not the product: the client is the storage operator. They own the units, nobody bills them, and rent settles to their own merchant. The SaaS layer is archived rather than deleted — it still compiles and is still covered by tests, but every one of its endpoints answers 404 while the flag is off.
| SaaS (archived) | Single operator (current) | |
|---|---|---|
| Who runs /super-admin | MiStorage, the platform | The client, at owner / HQ level |
| Who runs /tenant/{id} | A subscribing storage company | The client, at location level |
| What a Customer row is | A company paying a subscription | A location |
| Who signs up at /tenant/register | Any company, self-service, with a card | Nobody — staff accounts are created by an administrator |
| Where renter rent lands | The company's merchant, else the platform's | The client's own merchant |
| Subscriptions, plans, platform invoices | The business model | Archived behind the flag |
Saas:Enabled on the API (default false) is mirrored by NEXT_PUBLIC_SAAS_ENABLED in the frontend. The frontend value is baked in at build time, so changing it needs a rebuild. A mismatch is the worst outcome: a UI offering subscription plans that the server answers 404 for.
| Area | Choice |
|---|---|
| Runtime | .NET 10 / ASP.NET Core Web API (controllers) |
| ORM | Entity Framework Core 10 |
| Database | PostgreSQL 17 (Npgsql provider) |
| Auth | JWT bearer access tokens (15 min, held in memory) + hashed, rotating refresh tokens in an HTTP-only cookie |
| Password hashing | BCrypt, work factor 12 |
| Field encryption | AES-256-GCM — the renter's Social Security Number, and nothing else |
| Payments | PhoenixGate / QuickPayments, tokenized browser-direct — no card number ever reaches the API |
| API reference | OpenAPI + Scalar, mounted in Development only |
| Tests | xUnit — 243 tests, all passing as of 2026-09-17 |
| Piece | Runs on |
|---|---|
| API (mistorage-backend) | Azure App Service, West US 3 |
| Frontend (MiStorage, Next.js) | AWS Amplify |
| Database | AWS RDS, PostgreSQL 17 |
Middleware order in Program.cs: security headers → global exception handler → rate limiting → HSTS / HTTPS redirect (production) → CORS → authentication → SaaS feature gate → authorization → controllers. Request bodies are capped at 10 MB.
userType claim stamped at mint time: SystemUser, TenantUser and Renter. Each audience has its own refresh-token table, so a token minted for one can never be presented against another.Refresh tokens are hashed at rest and rotated on every use. A rotated token presented again outside a short grace window is treated as theft and revokes the whole family. See the APIs page for the session policy and the full endpoint reference.
What a rental application asks for (Social Security Number, driver's licence) is resolved by ApplicationFieldPolicyService from three levels — the unit, then its location, then the installation default. null means "no opinion, ask the level above"; false means "do not ask". Both fields are collected by default.
Work was planned as numbered phases in the customer-portal gap analysis; the pivot to single operator and its follow-ups sit outside that numbering.
| Phase | State |
|---|---|
| 0 — compliance / raw-PAN removal | Done. Credential rotation still owed. |
| 1 — unit size taxonomy | Done (2026-08-05) |
| 2 — public storefront API + UI | Done, live-verified (2026-08-10 / 08-12) |
| 3 — renter identity | Done, live-verified (2026-08-21) |
| 3.5 — password recovery (unplanned) | Done (2026-08-25). None of the three audiences had a reset path. |
| 4 — rental flow + renter billing | Done end to end (backend 08-25, frontend 09-01). The real card charge has never run. |
| 5 — renter portal + tenant RBAC | Renter half substantially done (09-08). Tenant RBAC not started. |
| 6 — DNS automation, proration, occupancy reporting | Not started |
| Single-operator pivot | Done (2026-09-10). SaaS archived behind Saas:Enabled. |
| Single-operator follow-ups | Done (2026-09-11): Locations rename, editable units, per-unit / per-location application fields. |
| Production catch-up, one owner many locations | Done (2026-09-15) |
| HQ ↔ location bridge | Done (2026-09-17), not yet deployed. |
Email is currently undeliverable (the SMTP provider rejects the configured sender), so verification links, password resets and notifications reach nobody — email verification is switched off as a consequence. The payment gateway sandbox is only reachable from the US, so the real card charge has never been exercised. Proration of the first month is a commercial decision still owed by the client.
So nobody spends time re-deriving them.
POST /api/renter-auth/revoke-all is the deliberate way to end all of them.