NEXUS
Product Overview

What is MiStorage?

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.

Who uses it

Three audiences, three token types, three front doors. Every request is scoped to one of them.

HQ (system users)The operator's owners and administrators behind /super-admin: locations, staff, roles, protection plans, notifications, tasks and settings.
Location staff (tenant users)The staff console for one location behind /tenant/{locationId}: units, renters, leases, notifications and the location's own settings.
RentersStorage customers on the public storefront at /s/{subdomain}: browse units, get a quote, rent online, then manage leases, invoices and addresses.

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.

Which product is this?

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-adminMiStorage, the platformThe client, at owner / HQ level
Who runs /tenant/{id}A subscribing storage companyThe client, at location level
What a Customer row isA company paying a subscriptionA location
Who signs up at /tenant/registerAny company, self-service, with a cardNobody — staff accounts are created by an administrator
Where renter rent landsThe company's merchant, else the platform'sThe client's own merchant
Subscriptions, plans, platform invoicesThe business modelArchived behind the flag

Tech stack

AreaChoice
Runtime.NET 10 / ASP.NET Core Web API (controllers)
ORMEntity Framework Core 10
DatabasePostgreSQL 17 (Npgsql provider)
AuthJWT bearer access tokens (15 min, held in memory) + hashed, rotating refresh tokens in an HTTP-only cookie
Password hashingBCrypt, work factor 12
Field encryptionAES-256-GCM — the renter's Social Security Number, and nothing else
PaymentsPhoenixGate / QuickPayments, tokenized browser-direct — no card number ever reaches the API
API referenceOpenAPI + Scalar, mounted in Development only
TestsxUnit — 243 tests, all passing as of 2026-09-17
Where it runs
PieceRuns on
API (mistorage-backend)Azure App Service, West US 3
Frontend (MiStorage, Next.js)AWS Amplify
DatabaseAWS RDS, PostgreSQL 17

How a request is handled

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.

  • CORS allows the configured frontend origins with credentials.
  • Rate limiting is per IP, in buckets grouped by surface so one audience can never exhaust another's allowance. Refresh is unmetered on purpose — it is cookie-authenticated and runs on a timer.
  • The SaaS gate runs before authorization, so an archived endpoint answers 404 to everyone rather than 401 or 400, which would advertise that it exists.
  • Authorization uses three policies keyed off a 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.

Core domains

Auth & RBACUsers, roles, permissions, refresh and password-reset tokens — separately for HQ, location staff and renters.
LocationsThe Customer entity, now meaning a location: profile, subdomain, status, bans, contact details and staff logins.
Storefront & rental flowPublic unit listings with a size guide, quotes, the three-step checkout, leases and renter invoices.
Units & leasesStorage units with a size taxonomy, climate control, rates and out-of-service state; leases with move-out notice and staff move-out.
NotificationsTemplates, recipient selection, previews, batches, history and replies — for HQ and for each location.
Protection plans, tasks, settings, auditRenter protection plans, HQ task board, system settings, and audit logs for sensitive actions.

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.

Where the project stands

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.

PhaseState
0 — compliance / raw-PAN removalDone. Credential rotation still owed.
1 — unit size taxonomyDone (2026-08-05)
2 — public storefront API + UIDone, live-verified (2026-08-10 / 08-12)
3 — renter identityDone, 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 billingDone end to end (backend 08-25, frontend 09-01). The real card charge has never run.
5 — renter portal + tenant RBACRenter half substantially done (09-08). Tenant RBAC not started.
6 — DNS automation, proration, occupancy reportingNot started
Single-operator pivotDone (2026-09-10). SaaS archived behind Saas:Enabled.
Single-operator follow-upsDone (2026-09-11): Locations rename, editable units, per-unit / per-location application fields.
Production catch-up, one owner many locationsDone (2026-09-15)
HQ ↔ location bridgeDone (2026-09-17), not yet deployed.

Decisions made on purpose

So nobody spends time re-deriving them.

  • The storefront is server-rendered and the rest of the app is not. A shop window has to be indexable and fast on a phone.
  • `/s/{subdomain}` instead of real subdomains. A stand-in until DNS automation (Phase 6); routing already keys off the subdomain segment.
  • Move-out is notice, not cancellation. A renter gives notice; the lease stays active and rent keeps accruing until staff end it.
  • Registration returns one identical acknowledgement for every outcome when email verification is on. That is an anti-enumeration measure, not a missing branch.
  • Signing in does not end a renter's other sessions. POST /api/renter-auth/revoke-all is the deliberate way to end all of them.
  • The API does not migrate on startup. Production schema changes are applied by hand from an audited, idempotent script.