Settings & AppConfig Sprint Plan — Cross-Functional War Room Output¶
Context: 25 issues from Settings & AppConfig Deep-Dive Generated: 2026-04-12 | Method: Cross-Functional War Room (PM + Engineer + Designer) Participants: PM (business impact), Engineer (effort/risk), Designer (UX impact) Related: Deep-Dive | Settings & Configuration Sprint Plan Scope Note: This plan covers the Setting entity (UI configuration) and AppConfig entity (server-side secrets) data flow analysis. It is complementary to — but distinct from — the Settings & Configuration sprint plan, which covered the broader UserSetting / 2FA / validation surface. Where overlap exists (e.g. AppConfig permission naming, hard-delete fixes), this plan defers to whichever bucket lands the fix first.
Frontend Progress Update (2026-04-13): All frontend items verified against
Sass-boilerplate-frontend/. ✅ Resolved: Ship #5 (C1), S1-1, S1-2, S1-3, S1-4, S1-5, S1-6, S1-7, S1-8, S2-1, S2-2, S2-3, S2-4, S2-5, S2-11 (closed as won't-fix after architectural review — see row for details). Backend items (C2, C3, C4, H4, M7, M8, H5, S2-7, S2-8) and documentation/test items (S2-9, S2-10, S2-12) remain open.
Triage Summary¶
| Bucket | Criteria | Count | Effort |
|---|---|---|---|
| Ship Blocker | Cannot deploy. Security exploit, data loss, broken core surface. | 8 | ~50 min |
| Sprint 1 | Settings caching/state works end-to-end. Type & cache consistency. | 8 | ~4 hrs |
| Sprint 2 | Code quality, dead code, fallbacks, docs, integration tests. | 8 | ~6 hrs |
| Backlog | Cosmetic, deferred, or future-Next.js migration. | 1 | Ongoing |
Issue Count by Severity (from deep-dive):
| Severity | Count | IDs |
|---|---|---|
| Critical | 4 | C1, C2, C3, C4 |
| High | 5 | H1, H2, H3, H4, H5 |
| Medium | 8 | M1, M2, M3, M4, M5, M6, M7, M8 |
| Low | 8 | L1, L2, L3, L4, L5, L6, L7, L8 |
| Total | 25 |
Ship Blockers (Pre-Release Gate)¶
Timeline: ~50 minutes, 1 developer Exit criteria: Settings GET endpoints authenticated. Dashboard survives a settings outage. All consumed setting keys exist in seed. AppConfig CRUD reachable for non-super_admin. No data loss on AppConfig delete.
| Order | Issue | Description | File | Change | Effort |
|---|---|---|---|---|---|
| 1 | C2 | Authenticate GET /settings/* routes |
setting.routes.ts:109-121 |
Move all 3 GET registrations (/, /:prefix/prefix, /:id) to AFTER router.use(auth(), contextMiddleware). If public access is genuinely required for SSR pages, mount a separate publicSettingRouter with an explicit allow-list of group prefixes (e.g. site, admin, auth, contact) only. |
10 min |
| 2 | C3 / H4 | Seed admin.admin_site_title |
seed/data/setting.ts |
Add row: { group: "admin", key: "admin.admin_site_title", value: "SaaS Boilerplate", display_name: "Dashboard Title", type: "text", order: 6 } |
2 min |
| 3 | C4 / H4 | Seed admin.meta_title |
seed/data/setting.ts |
Add row: { group: "admin", key: "admin.meta_title", value: "SaaS Boilerplate", display_name: "Page Meta Title", type: "text", order: 7 } |
2 min |
| 4 | H4 | Seed remaining footer + auth keys | seed/data/setting.ts |
Add 5 rows: site.footer_site_link, site.footer_site_name, site.footer_heart_link, auth.login_page_title, auth.register_page_title with sensible defaults matching the in-code fallbacks (https://yoursite.com, Your Site Name, Login, Register). |
5 min |
| 5 | C1 | ✅ RESOLVED 2026-04-13 — Stop blocking dashboard on settings load | dashboard/layout.js:39 |
Remove isSettingsLoading from the gate. Render the dashboard with whatever settings have arrived; let the title fall through to the existing ?? "Dashboard" fallback. Show a one-time react-hot-toast error if useSettings() enters error state. (Mirrors the FE-M7 fix already shipped in the configuration sprint plan.) |
10 min |
| 6 | M7 | Fix AppConfig permission naming | app_config.routes.ts:139-140 + seed/data/permission.ts |
Rename create_app_config.create → app_config.create and update_app_config.update → app_config.update. Add the new strings to the permission seed. Cross-check with settings-configuration-sprint-plan.md AC-C1/AC-C2 — if already shipped there, mark this RESOLVED. |
5 min |
| 7 | M8 | Fix AppConfig hard-delete | app_config.service.ts deleteAppConfig() |
Change configRepo.delete(id) → configRepo.softDelete(id). Entity already has @DeleteDateColumn. Cross-check with configuration sprint plan AC-C3 — defer if already shipped. |
1 min |
| 8 | H5 | Strip backupCodes + verifyBackupCode from Setting |
entity/Setting.ts:39, setting.service.ts |
Remove backupCodes column and the verifyBackupCode() method (it references a non-existent userId column on the Setting entity — calling it would crash). Add a TypeORM migration to drop the column. The real backup-code logic lives on UserSetting. |
15 min |
Verification Checklist¶
After completing all 8 ship blockers:
-
GET /settingswithout an auth header returns 401 (or returns only the public allow-list) - Dashboard loads with
<title>populated fromadmin.admin_site_titleafter a freshyarn seed - Root layout
generateMetadata()returns the seededadmin.meta_title(not the hardcoded fallback) - Footer-credit renders site link / site name / heart link from seed (no
algorizetech.comhardcoded fallbacks) - With the API down, the dashboard still renders (no infinite
<Loading fullScreen />); a toast surfaces the failure -
POST /app-configssucceeds for a user withapp_config.createpermission -
DELETE /app-configs/:idleaves the row in the DB withdeletedAtset -
Settingentity no longer compiles a reference tobackupCodesorverifyBackupCode
Sprint 1: Make Settings State Coherent (Week 1)¶
Timeline: ~4 hours, 1 developer North Star: A setting edited in the admin UI is reflected in every consumer (root layout, dashboard, public SSR pages) without a server restart or stale cache. Exit criteria: Single source of truth for SSR settings access. Cache invalidation reaches every layer. No type coercion bugs between frontend fallbacks and seeded values.
Caching & Invalidation (1.5 hrs)¶
| # | Issue(s) | Description | File(s) | Effort |
|---|---|---|---|---|
| S1-1 | H2 | ✅ RESOLVED 2026-04-13 — Delete fetchMetaData(), use shared getSettingByGroup. Root layout now imports getSettings from api/public/settings and calls it from generateMetadata(). fetchMetaData fully removed. |
app/layout.js:8,27-28 |
20 min |
| S1-2 | H1 | ✅ RESOLVED 2026-04-13 — Cache chain verified end-to-end using the stable Next.js 15 primitives: fetch() with next: { revalidate, tags } + revalidateTag(). Note: did NOT migrate to unstable_cache — that API is experimental and has known bugs. Current implementation tags group fetches with ["settings-group", "settings-<group>"] and mutations call revalidateBulk(["settings-group", "all-settings"]), which busts all grouped and all-settings cache entries in one call. Equivalent effect, stable API. |
app/layout.js, api/public/settings.js, actions/revalidate.js |
30 min |
| S1-3 | H3 | ✅ RESOLVED 2026-04-13 (moot) — Originally required because sidebar-old.jsx consumed useSettingByGroup("site"). Both sidebar-old.jsx AND the useSettingByGroup hook itself have been deleted (see S1-8). No remaining consumers → group-scoped invalidation has nothing to bust. Revisit only if a grouped consumer is reintroduced. |
api/settings/index.js |
15 min |
| S1-4 | L3 | ✅ RESOLVED 2026-04-13 — Replace deprecated keepPreviousData. useSettings() now uses placeholderData: keepPreviousData with proper import from @tanstack/react-query. (useSettingByGroup removed entirely in S1-8 follow-up.) |
api/settings/index.js:3,27 |
10 min |
Type & Naming Consistency (1 hr)¶
| # | Issue(s) | Description | File(s) | Effort |
|---|---|---|---|---|
| S1-5 | M3 | ✅ RESOLVED 2026-04-13 — Standardize website_type to string. register.jsx:52 now reads settings?.website_type \|\| "1". Backend strict equality with "2" now works correctly. |
register.jsx |
15 min |
| S1-6 | M5 | ✅ RESOLVED 2026-04-13 — Standardize setting type field to "text". create-setting-form.jsx SETTING_TYPES contains { value: "text", label: "Text Input" } and the switch in setting-field.jsx handles "text" directly. Frontend side fully resolved; backend seed update is tracked under the configuration sprint plan. |
create-setting-form.jsx, setting-field.jsx |
15 min |
| S1-7 | M6 | ✅ RESOLVED 2026-04-13 — Resolve getSettingByGroup name collision. Redux selector renamed to selectSettingByGroup in store/slices/settingSlice.js. All consumers (footer-credit.jsx, dashboard/layout.js) updated. SSR helper getSettingByGroup kept as the public API name. |
settingSlice.js, consumers |
15 min |
| S1-8 | M4 | ✅ RESOLVED 2026-04-13 — Delete sidebar-old.jsx. File deleted. Follow-up: also deleted the now-orphaned useSettingByGroup hook from api/settings/index.js since no consumers remain. |
dashboard/components/sidebar-old.jsx (DELETED), api/settings/index.js (hook removed) |
15 min |
Sprint 1 Acceptance Test¶
1. yarn seed → Seeds all 25+ setting keys (incl. ship-blocker additions)
2. Login as admin → Dashboard loads
3. Edit "admin.meta_title" in Site Settings → Save
4. Reload public homepage → New title visible WITHOUT server restart
5. Edit "site.footer_copyright" → Save
6. Reload public footer + dashboard footer → Both reflect new value
7. Edit a setting in the "site" group, then reload sidebar-old (if it existed) → Would refetch (S1-3)
8. Register page load → website_type comparison works as string
9. yarn build → No React Query v5 deprecation warnings
10. Search codebase for "sidebar-old" → No matches
Sprint 2: Clean Foundation (Week 2-3)¶
Timeline: ~6 hours, 1-2 developers North Star: Every consumer has a defined fallback. Dead code is gone. Documentation matches reality. First integration test exists for the Setting → SSR → consumer chain. Exit criteria: No empty renders on missing settings. Linting passes with no dead-code warnings. Architecture decision documented. One integration test green.
Frontend Polish (1 hr)¶
| # | Issue(s) | Description | Effort |
|---|---|---|---|
| S2-1 | L1 | ✅ RESOLVED 2026-04-13 — Add <Image> fallback in header.jsx. All 4 <Image> tags (site_logo + site_logo_dark, both pre-mount and post-mount branches) now use \|\| "/logo.png". /public/logo.png exists. |
5 min |
| S2-2 | L2 | ✅ RESOLVED 2026-04-13 — Add footer_copyright fallback in footer-credit.jsx. Falls back to "All rights reserved." when undefined; surrounding markup already renders © {year} + site link. |
5 min |
| S2-3 | L7 | ✅ RESOLVED 2026-04-13 — Replaced window.location.href in login.jsx (3 sites: JWT-from-URL effect, normal onSubmit, 2FA handler) with router.push() via a new shared useFinalizeLogin() hook in api/auth/index.js. Uncovered race condition: the original window.location.href path was masking a bug where useLogin.onSuccess seeded queryClient.setQueryData(["user"], data.data.user) with an incomplete user shape (no permissions); with useProfile's staleTime: 5 * 60 * 1000, React Query returned the stale seeded user and skipped the real /users/profile call. useFinalizeLogin fixes this by explicitly calling authAPI.getProfile(), seeding both Redux and RQ with the authoritative profile, checking dashboard.view permission, and routing to /dashboard or /dashboard/settings/profile accordingly. On failure: clears cookie, dispatches logout, routes to /login. Also applied to useLogout (router.push("/login")). |
15 min → 45 min (due to race-condition fix) |
| S2-4 | M1 | ✅ RESOLVED 2026-04-13 — settinReducer typo already fixed in store/index.js (imports and uses settingReducer). Confirmed via cross-check with configuration sprint plan FE-L3. |
2 min |
| S2-5 | M2 / L5 | ✅ RESOLVED 2026-04-13 — Console error messages in api/public/settings.js are adequate and unambiguous: "Failed to fetch settings group:" (L15), "Error fetching settings group:" (L30), "Failed to fetch settings:" (L54). Original sprint wording assumed a different code shape; current messaging is acceptable. |
2 min |
| S2-6 | L6 | Fix updateSettingById controller success message. setting.controller.ts:79 → "Setting updated successfully". Cross-check with configuration sprint plan SET-M1 — defer if already shipped. |
1 min |
Backend Cleanup (45 min)¶
| # | Issue(s) | Description | Effort |
|---|---|---|---|
| S2-7 | L4 | Delete getImageSizeByKey() from setting.service.ts. Confirm zero call sites first. If a future feature needs max-image-size, it can be reintroduced via a normal getSettingByKey("admin.image_size") lookup. |
10 min |
| S2-8 | H5 follow-up | After ship-blocker H5 lands, ship the migration: drop the backupCodes column from the settings table. Verify no production data depends on it. |
30 min |
Architecture & Documentation (2 hrs)¶
| # | Description | Effort |
|---|---|---|
| S2-9 | Document the Setting vs AppConfig vs UserSetting decision. Create docs/architecture-settings.md explaining: (a) Setting = public UI config (group.field key format, dot-split transform, dual SSR + CSR access); (b) AppConfig = server-side secrets, never exposed to frontend, encrypted; © UserSetting = per-user 2FA state. Include the data-flow diagram from the deep-dive section 10. |
1 hr |
| S2-10 | Document the public-GET decision (or undo it). Settings GET routes are now authenticated (ship-blocker C2). If SSR pages still need public access, document the public allow-list and the threat model in docs/security-settings.md. Tie the decision to the registration policy (auth.website_type) which currently leaks publicly. |
30 min |
| S2-11 | ✅ RESOLVED 2026-04-13 — Won't fix (premise disproved). Spike ran and found the Redux settingSlice is not a redundant mirror of React Query — it is the SSR hydration bridge for public/auth pages (populated by ThemeProvider from the prop passed by root app/layout.js) AND the live-update bridge for in-dashboard edits (populated by dashboard/layout.js on useSettings() refetch). Removing either dispatch introduces a real UX regression: dashboard consumers (footer-credit.jsx, theme-control.jsx) would show stale values until a hard page reload after an admin edit. TanStack Query alone cannot replace this because (a) useSettings() hits the authenticated endpoint and can't be called from public/auth pages, and (b) the public SSR helper getSettingByGroup() is server-only. An explanatory comment has been added to the setSettings dispatch in dashboard/layout.js so the non-obvious purpose isn't lost. Cross-reference with configuration sprint plan S2-22 — same conclusion applies there. |
(closed) |
Testing (2 hrs)¶
| # | Description | Effort |
|---|---|---|
| S2-12 | First integration test for Settings. Set up Jest + ts-jest if not already done by the configuration sprint plan. Write one end-to-end test: seed a setting → call GET /settings/admin/prefix → assert the response has the expected dot-split shape → call PATCH /settings/:id → assert revalidateTag was called. This is the single most important regression preventer for the SSR cache chain. |
2 hrs |
Backlog (Ongoing)¶
Fix opportunistically when touching adjacent files. No dedicated sprint time needed.
| Issue | Description | Fix When |
|---|---|---|
| L8 | unstable_cache is labeled experimental in next/cache. Add a TODO comment and migrate when Next.js ships a stable replacement. |
Next.js stable cache API ships |
| Architecture | N/A | |
| Architecture | useSettingByGroup hook has zero remaining consumers after S1-8 (sidebar-old deleted). Consider deleting the hook itself in a follow-up cleanup. |
Next time api/settings/index.js is touched |
| SSR vs CSR | Public SSR consumers (getSettingByGroup) and authenticated CSR consumer (useSettings) hit different endpoints with different cache keys. Acceptable today, document in S2-9. |
Documented in Sprint 2 |
Cross-Plan Overlap with settings-configuration-sprint-plan.md¶
This deep-dive partially overlaps with the earlier configuration sprint plan. Where the same issue is addressed in both, fix once and mark RESOLVED in both plans. Known overlaps:
| This plan | Configuration plan | Status to verify |
|---|---|---|
| Ship #5 (C1 — non-blocking dashboard) | FE-M7 (RESOLVED Apr 11b) | Likely already shipped |
| Ship #6 (M7 — AppConfig perm naming) | AC-C1 (Ship Blocker) | Defer if shipped |
| Ship #6 seed addition | AC-C2 (Ship Blocker) | Defer if shipped |
| Ship #7 (M8 — AppConfig soft delete) | AC-C3 (Ship Blocker) | Defer if shipped |
| S1-7 (M6 — selector rename) | FE-M6 (RESOLVED Apr 11b) | Likely already shipped |
S2-4 (M1 — settinReducer typo) |
FE-L3 | Likely already shipped |
| S2-6 (L6 — controller message) | SET-M1 | Defer if shipped |
Action item before starting: run git log --oneline docs/extra-docs/settings-configuration-sprint-plan.md and the actual code paths to mark the above as RESOLVED before beginning the ship-blocker work. Saves ~20 min of duplicate effort.
Key Decisions Log¶
| Decision | PM | Engineer | Designer | Outcome |
|---|---|---|---|---|
| Fix order for ship blockers | Security first (C2), then visible breakage (C1) | Agree — C2 is 10 min, C1 is 10 min | Dashboard infinite loading is the most user-visible | Security → seed → dashboard → AppConfig |
| Public GET routes — keep or auth | Auth them. Public allow-list only if SSR truly requires it. | Easy to add allow-list mount | SSR public pages need site/auth/contact groups | Authenticate by default, add narrow public mount if needed |
| Seed missing keys vs add fallbacks | Seed them — admin can edit later | Seed is 5 min, fallbacks scattered | Admin expectation is "edit, see change" | Seed |
Remove backupCodes from Setting |
Sprint 0 — dead column, no risk | Sprint 0 — migration in Sprint 2 | Invisible | Ship blocker drop, Sprint 2 migration |
Root layout unstable_cache migration |
Sprint 1 — fixes the "edit doesn't reflect" bug | Sprint 1 — depends on H2 cleanup | High user impact for admins | Sprint 1 |
| Redux removal | Sprint 2 spike — share with configuration plan | One spike covers both | N/A | Sprint 2 (single shared spike) |
| First integration test | Sprint 2 — needs framework setup | Sprint 2 — ~2 hrs | N/A | Sprint 2 |
Sprint Board Visualization¶
+-----------------------------------------------------------------+
| SHIP BLOCKERS (pre-release) |
| 8 items . ~50 min . 1 developer |
| |
| [SEC] C2 (auth GET) |
| [SEED] C3 C4 H4 (7 missing keys) |
| [BUG] C1 (infinite loading) |
| [AC] M7 (perm naming) M8 (soft delete) — defer if shipped |
| [DM] H5 (backupCodes column drop) |
| |
| EXIT: Settings authenticated. Dashboard survives outage. |
+-----------------------------+-----------------------------------+
v
+-----------------------------------------------------------------+
| SPRINT 1 -- "State Coherent" (week 1) |
| 8 items . ~4 hrs . 1 developer |
| |
| [CACHE] H2 (delete fetchMetaData) H1 (unstable_cache) H3 (key) |
| [DEPR] L3 (placeholderData) |
| [TYPE] M3 (website_type) M5 (text vs input) |
| [NAME] M6 (selector rename) M4 (delete sidebar-old) |
| |
| EXIT: Edit -> save -> reload reflects everywhere. |
+-----------------------------+-----------------------------------+
v
+-----------------------------------------------------------------+
| SPRINT 2 -- "Clean Foundation" (week 2-3) |
| 8 items + tests + docs . ~6 hrs |
| |
| [FE] L1 (header img) L2 (copyright) L7 (router) |
| M1 (typo) M2/L5 (log msg) L6 (msg) |
| [BE] L4 (dead method) H5 migration |
| [DOCS] S2-9 (architecture) S2-10 (public-GET decision) |
| [TEST] Integration test (most important item) |
| |
| EXIT: Fallbacks everywhere. Docs match. Test passes. |
+-----------------------------+-----------------------------------+
v
+-----------------------------------------------------------------+
| BACKLOG (ongoing) |
| 1 item . fix when Next.js stabilizes |
| |
| L8 (unstable_cache migration) |
| + dual-access-pattern simplification (covered by S2-11) |
+-----------------------------------------------------------------+
Total Effort Estimate¶
| Phase | Items | Effort | Developers | Timeline |
|---|---|---|---|---|
| Ship Blockers | 8 (1 FE ✅, 7 BE open) | ~50 min | 1 | Day 1 (before any deployment) |
| Sprint 1 | 8 (8 FE ✅, 0 open) | ~4 hrs | 1 | Week 1 |
| Sprint 2 | 8 + tests + docs (5 FE ✅, 3 BE/docs/tests open) | ~6 hrs | 1-2 | Week 2-3 |
| Backlog | 1 | N/A | — | Ongoing |
| Total | 25 (14 resolved 2026-04-13, 11 open) | ~11 hrs (~6 hrs remaining) | ~3 weeks |
Resolved as of 2026-04-13: Ship #5 (C1), S1-1, S1-2, S1-3, S1-4, S1-5, S1-6, S1-7, S1-8, S2-1, S2-2, S2-3, S2-4, S2-5. All frontend work complete.
Still open: Backend ship blockers (C2, C3, C4, H4, M7, M8, H5), backend cleanup (S2-7, S2-8), architecture/docs (S2-9, S2-10, S2-11), testing (S2-12), backlog (L8).
Several items overlap with the Settings & Configuration Sprint Plan. Verify status before starting — overlap could trim ~30-45 min from the ship-blocker bucket.
Generated by BMAD Cross-Functional War Room workflow — Settings & AppConfig Data Flow, 2026-04-12 Cross-reference: Deep-Dive: Settings & AppConfig