SKUSeer
A shop with four hundred SKUs does not need a data team, it needs to know what to reorder on Tuesday. SKUSeer takes a sales history, forecasts demand per product, and puts that behind a permission model granular enough that the weekend stand-in can see stock levels without being able to change anyone's access.
Forecasting engine by Bilal Eddinaoui. The tenant model, permission system, API and security work described here are mine.
- 20
- distinct permissions across 7 domains
- 13
- API surfaces, from imports to forecasts
- 3
- services behind one identity provider
What it is
Three services behind one login: an API that owns the tenant model and the business rules, a forecasting engine that turns sales history into per-product demand predictions, and a React front end. Keycloak sits in front as the identity provider, so authentication is delegated rather than reimplemented and the API's job is reduced to trusting a verified token.
A tenant is a store. Everything below it — products, categories, sales history, imports, forecast runs, promotions, reports, integrations — belongs to that store and is invisible to every other one.
Roles the store owner defines, not the developer
Most small-business software ships three fixed roles and hopes one of them fits. Real shops do not divide that way: the bookkeeper needs reports but not stock edits, the weekend stand-in needs the dashboard and nothing else, the owner's business partner needs everything except the ability to remove the owner.
So permissions are the primitive, not roles. Twenty of them, named for what they let you do — view products, import products, retrain a forecast, schedule a report, manage members, manage roles — and a role is a named set of permissions that belongs to one tenant. Two stores can both have a role called 'Manager' that means different things, because the uniqueness constraint is on the pair, not the name.
- Permissions are checked per request against the caller's membership in the tenant being addressed, so a valid token for one store grants nothing in another.
- The platform-level administrator role lives in the identity provider's realm, not in a tenant — a store owner cannot grant it to themselves.
What a security review changed
This is the part of the project worth reading. Reviewing the authentication path turned up a genuine account-takeover route: the API provisioned a user row on first login and would attach an incoming identity to an existing placeholder row matched by email address. An attacker who registered an unverified account with a known staff member's email address inherited that row and its tenant memberships.
The fix was to claim a placeholder only when the provider asserts the email is verified, which moves the trust decision to the only component that can actually make it. Two related gaps closed alongside it: the token's authorised-party claim was not checked, so a token minted for a different client in the same realm was accepted; and CSV export wrote user-controlled cell values unescaped, letting a crafted product name execute as a formula when the file was opened in a spreadsheet.
- Verify the authorised party and audience, not just the issuer and signature.
- Neutralise leading formula characters on export — a spreadsheet is a code execution environment (CWE-1236).
- Cap request bodies before parsing, so a large upload is refused rather than buffered.
- The shared demo account is provisioned one rung down from administrator, so a public demo cannot delete the store or change roles.
Why it isn't a public demo
Keycloak, Postgres, the API and the forecasting engine are four long-running processes with persistent state — comfortably past what a free tier allows. The deployment is written and waiting: a reverse proxy terminating TLS, every service bound to localhost, Keycloak in production mode behind forwarded headers, and the realm rendered at deploy time from the committed development realm so the public client trusts exactly one origin and the demo account loses its administrator role.
Rendering rather than maintaining two realm files was the deliberate choice there. Two copies of an identity configuration drift, and the drift is invisible until it is a security incident.
Built with
- FastAPI
- PostgreSQL
- Keycloak
- OIDC
- React
- Vite
- TanStack Query
- Docker Compose
Not hosted: four stateful services behind an identity provider. The production deployment is written but needs a server.