A demo that only shows screenshots convinces nobody. We wanted visitors to open the real OpsAPI admin dashboard and click around. That means publishing a password, and three things can go wrong: visitors see other people's data, visitors change things, and visitors lock the next visitor out.
1. Keep real people out of view
The apps in the demo let anyone sign in with Google, and those accounts go to a workspace called demo. If the published login could read that workspace, its user list would show the real email address of everyone who had tried the apps. So the published login lives somewhere else: a showcase workspace holding only fictional example.com users, owned by a fictional user too, so not even the owner's address is real.
2. Let visitors see everything and change nothing
OpsAPI's roles are lists of module.action permissions per workspace. The seeded owner role has manage on every module; the demo login gets a viewer role built from the same module list with only read:
-- seed-showcase.lua
local read = {}
for module in pairs(cjson.decode(owner_role.permissions)) do read[module] = { "read" } end
-- => {"users":["read"],"roles":["read"],"settings":["read"], ...}Every page in the dashboard opens; every write is refused by the permission middleware before it reaches a query:
POST /api/v2/users (as demo@opsapi.org in showcase)
403 {"error":"Permission denied","required":{"module":"users","action":"create"}}3. Survive visitors who try anyway
Some actions belong to every signed-in user, whatever their role: changing your own password or deactivating your own account. A visitor could use either to lock everyone else out. A CronJob puts the account back every 30 minutes: the password hash of the published password, the active flag, and the viewer role.
4. No emailed code, in one workspace only
Members are asked for an emailed code when any workspace they belong to requires one. Nobody can read the demo account's inbox, so showcase has the code switched off. The private demo workspace keeps it on, and platform admins are always asked regardless.
Checklist
| Risk | Control | How we tested it |
|---|---|---|
| Visitors see real people | Separate showcase workspace, fictional owner | Member list over the public URL shows only example.com addresses |
| Visitors read the private workspace | Membership check per workspace | Request with X-Namespace-Slug: demo returns 403 |
| Visitors change data | viewer role: read only | Creating a user returns 403 Permission denied |
| Visitors lock the account | 30-minute reset CronJob | Ran the job by hand; the published password signs in afterwards |
| Inbox nobody reads | Emailed code off in showcase only | Sign-in returns a token with no 2FA step |
One edge lesson
The first version failed every read with "Access denied to this namespace". The edge proxy had been set to add X-Namespace-Slug: demo to every request for the identity host, which overwrote the header the dashboard sent and pinned every call to the private workspace. Tenancy belongs to the client and the token, not to a proxy rule; removing the header fixed it.
Try it on the live demo.