Running a Local MIB Stack — Gotchas & Fixes
Hard-won notes from standing up a full local MIB CMS from scratch — SQL Server +
MibMigrator + the auth / api / permission / frontend services + the React
shell. Each entry is symptom → cause → fix, and every item below was reproduced
and verified on a clean install (MIB 6.0.x images, MibFrontEnd main). They are the
things that are easy to lose an afternoon on and are not obvious from the
per-component install pages.
Auth engine. Since MEDIAIBOX-11999 the Authorization Server runs on OpenIddict (earlier 6.0.x images used IdentityServer4, EOL). This changes §6 — consent is now automatic — see the note there. For the current model see Authorization Server — Overview.
See also: Get Started With Docker · MibMigrator · Frontend — Local Development · Frontend Hands-On.
1. MibMigrator: assembly order and the two-pass requirement
Symptom. Running every migration assembly in a single invocation fails almost
immediately at the first Core migration (Mib_FirstMigration) with:
Migration error: ... SELECT * FROM [MEDIA_TYPES] ... : Invalid object name 'MEDIA_TYPES'.
Cause. MediaiBox.Cms.FrontEnd.Database.Migrations and
MediaiBox.Cms.DefaultPages.Database.Migrations carry MediaTypeCreator seeds that
read MEDIA_TYPES. When they are loaded together with the foundation assemblies, the
seed can run before the Core schema that creates MEDIA_TYPES is committed.
Fix. Run the migrator in two passes, each to HEAD:
- Foundation — in this order:
MediaiBox.Core→MediaiBox.Cms.DataModel→MediaiBox.ContentCriteria→MediaiBox.Cms.Api→MediaiBox.Cms.Authorization - Frontend — after pass 1 commits:
MediaiBox.Cms.FrontEnd→MediaiBox.Cms.DefaultPages
Domain scope. Running
Cms.DataModeltoHEADinstalls the full domain (VOD, series/seasons/episodes, music, EPG). For a generic base only, stop that assembly at202212231703060_Mib_GenericModelSeed— the last revision before202212261436570_Mib_VODGenericModel.
2. MibMigrator: SQL Server connection quirks
- Put the port inside
<server>. For SQL Server the migrator effectively uses only the<server>value; a separate<port>element is ignored. Use<server>localhost,14333</server>(and keep<type>sql2005</type>). Otherwise it silently connects to whatever is on the default1433and you getLogin failed for user 'sa'. - SQL Server is the supported target. PostgreSQL fails during seed execution
(e.g.
ADM_USERS.PASSWORD varchar(32)is too small for a seeded value /value too long for type character varying(32)). - SA password complexity. Fresh
mcr.microsoft.com/mssql/servercontainers reject simple passwords — use a mixed-case + digit value.
3. Creating a custom media type in a migration: the 4 base fields
When a migration creates a new media type, its ADM_FIELDS rows must start with
the four base fields — ID, DATEINS, NAME, OWNER. If they are missing, the BFF
throws on the edit page:
AdmFieldMisconfigurationException: Column 'DATEINS' was not found on Media Type X
Also note that MEDIA_TYPES, ADM_FIELDS, and ADM_RELATEDS carry
MIB_LAST_UPDATE as NOT NULL — set it on every insert. (API_CLIENTS likewise
has MIB_LAST_UPDATE and SECURITY_LEVEL_PERMISSION as NOT NULL; ADM_FIELDS also
requires READONLY and MINIMUM_SECURITY_LEVEL.)
4. React shell: config.json is served from /tmp, not /app
Symptom. The CMS redirects to a nonsense, looping URL like
https://<host>/undefinedauth/undefinedauth/.../login.
Cause. The runtime config.json is 404. The image's nginx serves it with
location = /config.json { root /tmp; }. If the container start script writes the
file to /app/config.json, it is never served; the SPA's getConfig() then resolves
AUTH_URL / API_URL to undefined, shouldUseOauthFlow() becomes false, and the
app falls back to the cookie login URL `${apiUrl}auth/login` — i.e.
undefinedauth/login, which is relative and loops.
Fix. Write the runtime config to /tmp/config.json.
5. React shell: the OAuth flow reads client creds from localStorage
shouldUseOauthFlow() checks mib_client_id / mib_client_secret in
localStorage (via getOauthCredentials()), not config.json. If your startup
only writes config.json, the OAuth flow never triggers and the app uses the cookie
flow instead. Seed the credentials into localStorage at startup — e.g. inject
<script>localStorage.setItem('mib_client_id', '<client>');
localStorage.setItem('mib_client_secret', '<secret>');</script>
into index.html (some image start.sh versions stopped doing this). authHost
must also be set (from config.json) for shouldUseOauthFlow() to return true.
6. Consent (legacy IdentityServer4 only — resolved under OpenIddict)
Resolved by the OpenIddict migration (MEDIAIBOX-11999). With OpenIddict and
useNewLoginUI=true, consent for first-party MIB clients is automatic/inline and the~/consentpage no longer exists — the symptom below does not occur and no pre-grant step is needed. The rest of this entry applies only to pre-migration IdentityServer4 stacks.
Symptom (IdentityServer4 only). Credentials are accepted, but instead of returning to the
app the flow redirects to /consent, which the SPA cannot render — so login appears to fail.
Cause. IdentityServer4's client default is RequireConsent = true (Duende later defaulted
it to false). API_CLIENTS has no consent column, so this can't be toggled per client from
data alone.
Fix. Pre-grant consent once through the real flow: log in and click Accept
with remember consent checked. That persists a user_consent row in AUTH_GRANTS
for the subjectId + clientId; every subsequent login is seamless. (Automate it
with a one-time scripted login that posts the consent form.)
7. The default Administrator has no group and isn't an auth admin
After a fresh migrate, the seeded Administrator user is in no group
(AUTH_USER_GROUP is empty) and IS_AUTH_ADMIN is NULL. Result: authorized API
calls return 403 and the CMS menu is empty.
Fix. Add Administrator to the Administrators group (insert into
AUTH_USER_GROUP) and/or set ADM_USERS.IS_AUTH_ADMIN = 1, then grant the relevant
AUTH_PERMISSIONS (per API_CLIENT_ID + group, RESOURCE_OWNER_TYPE = 2).
8. nginx header buffers vs. auth cookies
Symptom. 400 Request Header Or Cookie Too Large at /oauth/callback.
Cause. The auth/BFF session cookies (.AspNetCore.Cookies) and the OAuth
state/correlation cookies — especially after a bad redirect that bloats the returnUrl
state — can exceed nginx's default 8 KB header buffer. (Engine-agnostic: applies to both
OpenIddict and the legacy IdentityServer4.)
Fix. Raise the buffers on the frontend nginx (http or server context):
large_client_header_buffers 16 64k;
client_header_buffer_size 64k;
If a browser is already stuck in a redirect loop, also clear cookies for the affected
*.localtest.me hosts — a leftover oversized cookie can still trip the auth server's
own Kestrel header limit.
9. The React CMS menu stays empty — the configurable-permissions catalog
Symptom. After a fresh migrate the sidebar is empty. Even once you seed MIB3UX_*
pages for your media type, set IS_REACT = 1, add Administrator to a group, and grant
AUTH_PERMISSIONS, the menu endpoint (/api/v2/menu) still returns {"items":[]}. The
frontend log shows, per item:
Warning: Could not locate permission for menu item <menu_key>
Cause. Menu visibility is not driven by AUTH_PERMISSIONS alone. The BFF's
MenuTreeBuilder includes an item only when it finds a permission whose Key equals the
item's MENU_KEY and whose Type is Boolean, resolving true. Those Boolean
permissions are served by the permission microservice, which loads the set of
configurable permissions from each API client's CONFIGURABLE_PERMISSIONS_URL. A
fresh DB has no such catalog entry for your menu keys, so the permission is null and the
item is dropped (hence the log line). (MenuTreeBuilder.BuildMenuItemTree; the menu
permissions are read for the client in MIBAUTHORIZATIONCLIENTCONFIG_DEFAULT_CLIENTID.)
Fix. Three things must line up for each React menu item:
MIB3UX_MENU.IS_REACT = 1(the React menu only returns React items — see §10).- A configurable-permissions catalog entry: a Boolean permission whose
keyequals theMENU_KEY(the stock UI uses"category":"menu"), served from the BFF client'sCONFIGURABLE_PERMISSIONS_URL. - An
AUTH_PERMISSIONSgrant of that key (DATA = {"value":true},DATA_TYPE = 3) for the user's group (RESOURCE_OWNER_TYPE = 2) under that API client.
The catalog is a JSON document the permission microservice fetches:
{ "items": [
{ "id": "brands", "key": "brands", "name": "Brands", "title": "Brands",
"parent": null, "type": "Boolean", "category": "menu" },
{ "id": 5000, "key": "mediatype_5000", "name": "AGILEONE_BRANDS", "title": "Brands",
"parent": null, "type": "MediaType", "category": "" }
] }
Include a Boolean/menu entry per menu key and a MediaType entry per media type
(key = mediatype_<MIBINDEX>) for CRUD checks. Point the BFF client's
CONFIGURABLE_PERMISSIONS_URL at it (e.g. http://<nginx>/configurable-permissions.json,
served by your reverse proxy on the internal network).
Caching. The permission microservice caches the catalog (see
CONFIGURABLEPERMISSIONCACHETIME). After changing the catalog or the grants, restart the permission microservice and the BFF — DB/file edits alone won't show until the cache expires.
Cms.DefaultPages ships only legacy sample pages, so a navigable React CMS still needs
its own MIB3UX_* page/component rows per media type (see
Frontend Hands-On); the catalog above is what makes those
pages actually appear in the menu.
10. React menu items: IS_REACT, /Display = legacy, and formatAppPath
One MIB3UX_MENU table feeds both the legacy MVC sidebar and the React shell. The
routing rules are easy to get wrong:
IS_REACTselects the shell./api/v2/menu(React) returns onlyIS_REACT = 1items; the legacy menu returnsIS_REACT = 0. A page meant for the React shell that is left atIS_REACT = 0simply never appears in it.~/Display/*is the legacy MVC frontend. In the React shell,formatAppPathtreats any path starting with/Displayas external and builds a full legacy URL. So a~/Display/...menu URL withIS_REACT = 1bounces the user to the legacy frontend — not a React page. Build React menu/EDIT_URLvalues as base-relative React routes, never~/Display/....- Base path.
formatAppPathstrips the shell's base prefix (e.g./app/) →/before handing the path to React Router, whosebasenameis that same base. So a menu URL/app/<pageKey>renders the React page<pageKey>, and/app/<pageKey>/<id>hits the edit (content) route. The base comes fromBASE_PATH(config.json) and the build--base. (api-connectorformatAppPath;services/menuparseMenuItemruns every item'spaththrough it.) - Mounting under a base affects OAuth. If you serve the SPA under a base (e.g.
/app), the OAuthredirect_urimust include it (/app/oauth/callback) and matchAPI_CLIENTS.REDIRECT_URI, because the callback route lives under the routerbasename.
11. nginx caches upstream IPs — restart it last
Symptom. 502 Bad Gateway from the frontend nginx (connect() failed (111: Connection refused) ... upstream: http://<ip>:8603) even though the target container is
up and healthy.
Cause. With proxy_pass http://<service-name>:port (no resolver + variable), nginx
resolves each upstream's IP once at startup. Restart a backend after nginx and the
backend gets a new container IP while nginx keeps the stale one → connection refused.
Fix. Order matters: start/restart the backends first, then restart nginx last
so it re-resolves. (Alternatively use a resolver with a variable proxy_pass to
re-resolve per request.)
TL;DR boot order for a working local stack
- SQL Server up (complex SA password; create the target DB).
- MibMigrator pass 1 (foundation → HEAD), then pass 2 (FrontEnd + DefaultPages → HEAD).
- Seed OAuth clients (
API_CLIENTS),AUTH_PERMISSIONS, and theAdministratorgroup membership /IS_AUTH_ADMIN. - Start the services; write the React
config.jsonto/tmpand seed the OAuth creds intolocalStorage; raise nginx header buffers. - (Legacy IdentityServer4 only) pre-grant OAuth consent once — not needed on OpenIddict (consent is automatic).
- Seed your
MIB3UX_*CMS pages (see Frontend Hands-On) — React:IS_REACT = 1, base-relative URLs (/app/<pageKey>), never~/Display/.... - Publish the configurable-permissions catalog (a Boolean entry per menu key +
a
MediaTypeentry per media type), point the BFF client'sCONFIGURABLE_PERMISSIONS_URLat it, grant the keys, and restart the permission microservice + BFF (the catalog is cached). - Restart nginx last so it re-resolves upstream IPs.