Table of Contents

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:

  1. Foundation — in this order: MediaiBox.CoreMediaiBox.Cms.DataModelMediaiBox.ContentCriteriaMediaiBox.Cms.ApiMediaiBox.Cms.Authorization
  2. Frontend — after pass 1 commits: MediaiBox.Cms.FrontEndMediaiBox.Cms.DefaultPages

Domain scope. Running Cms.DataModel to HEAD installs the full domain (VOD, series/seasons/episodes, music, EPG). For a generic base only, stop that assembly at 202212231703060_Mib_GenericModelSeed — the last revision before 202212261436570_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 default 1433 and you get Login 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/server containers 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.

Resolved by the OpenIddict migration (MEDIAIBOX-11999). With OpenIddict and useNewLoginUI=true, consent for first-party MIB clients is automatic/inline and the ~/consent page 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:

  1. MIB3UX_MENU.IS_REACT = 1 (the React menu only returns React items — see §10).
  2. A configurable-permissions catalog entry: a Boolean permission whose key equals the MENU_KEY (the stock UI uses "category":"menu"), served from the BFF client's CONFIGURABLE_PERMISSIONS_URL.
  3. An AUTH_PERMISSIONS grant 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_REACT selects the shell. /api/v2/menu (React) returns only IS_REACT = 1 items; the legacy menu returns IS_REACT = 0. A page meant for the React shell that is left at IS_REACT = 0 simply never appears in it.
  • ~/Display/* is the legacy MVC frontend. In the React shell, formatAppPath treats any path starting with /Display as external and builds a full legacy URL. So a ~/Display/... menu URL with IS_REACT = 1 bounces the user to the legacy frontend — not a React page. Build React menu/EDIT_URL values as base-relative React routes, never ~/Display/....
  • Base path. formatAppPath strips the shell's base prefix (e.g. /app/) → / before handing the path to React Router, whose basename is 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 from BASE_PATH (config.json) and the build --base. (api-connector formatAppPath; services/menu parseMenuItem runs every item's path through it.)
  • Mounting under a base affects OAuth. If you serve the SPA under a base (e.g. /app), the OAuth redirect_uri must include it (/app/oauth/callback) and match API_CLIENTS.REDIRECT_URI, because the callback route lives under the router basename.

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

  1. SQL Server up (complex SA password; create the target DB).
  2. MibMigrator pass 1 (foundation → HEAD), then pass 2 (FrontEnd + DefaultPages → HEAD).
  3. Seed OAuth clients (API_CLIENTS), AUTH_PERMISSIONS, and the Administrator group membership / IS_AUTH_ADMIN.
  4. Start the services; write the React config.json to /tmp and seed the OAuth creds into localStorage; raise nginx header buffers.
  5. (Legacy IdentityServer4 only) pre-grant OAuth consent once — not needed on OpenIddict (consent is automatic).
  6. Seed your MIB3UX_* CMS pages (see Frontend Hands-On) — React: IS_REACT = 1, base-relative URLs (/app/<pageKey>), never ~/Display/....
  7. Publish the configurable-permissions catalog (a Boolean entry per menu key + a MediaType entry per media type), point the BFF client's CONFIGURABLE_PERMISSIONS_URL at it, grant the keys, and restart the permission microservice + BFF (the catalog is cached).
  8. Restart nginx last so it re-resolves upstream IPs.