-
Notifications
You must be signed in to change notification settings - Fork 31
ADR for backend data initialization #3734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,157 @@ | ||||||||||||||||||||||||||||||
# ADR: Move most initial data loading to app-backend | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- **Status:** Proposed | ||||||||||||||||||||||||||||||
- **Date:** 2025-09-23 | ||||||||||||||||||||||||||||||
- **Owner:** Team Studio | ||||||||||||||||||||||||||||||
- **Related:** Altinn/altinn-studio#16309 (Epic) ([GitHub][1]) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Context | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Today `app-frontend` bootstraps by mounting a stack of query providers (TanStack Query), each fetching pieces of application/instance/party/config data on load. This creates: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Many requests and duplicated invalidation logic | ||||||||||||||||||||||||||||||
- Complex provider/effect nesting and brittle boot order | ||||||||||||||||||||||||||||||
- Longer TTFB/TTI and heavier cognitive load for app developers | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Epic #16309 proposes shifting _initial_ data orchestration to `app-backend` and delivering a consolidated payload to the client at bootstrap, reducing round trips and complexity. ([GitHub][1]) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Decision | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
We will: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
1. **Generate the bootstrap HTML in `HomeController` (app-backend).** | ||||||||||||||||||||||||||||||
Backend takes ownership of serving the app shell (replaces handcrafted Index.cshtml). | ||||||||||||||||||||||||||||||
2. **Adopt path-based routing (no `HashRouter`).** | ||||||||||||||||||||||||||||||
Backend resolves/serves deep links, delegating route rendering to `app-frontend`. Back-compat preserved for legacy hash URLs. | ||||||||||||||||||||||||||||||
3. **Embed immutable “AppData” in the HTML.** | ||||||||||||||||||||||||||||||
Static, versionable, cross-variant data is serialized into a global (e.g., `window.AltinnAppData = {...}`) in the bootstrap page. | ||||||||||||||||||||||||||||||
4. **Provide an “InitialInstanceData” bundle when opening an existing instance.** | ||||||||||||||||||||||||||||||
When backend detects an instance context, it embeds (or exposes via single endpoint) the static-at-boot instance data adjacent to `AppData`, so the client does not immediately refetch/invalidate. Further interactions use targeted APIs. ([GitHub][1]) | ||||||||||||||||||||||||||||||
5. **Remove RuleHandler/RuleConfiguration in this major.** | ||||||||||||||||||||||||||||||
Do not migrate them to the new boot flow (handled by separate task). | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
In scope: catalog and migrate all initial queries across stateless, stateless-anon, and stateful apps. Out of scope: data that must be updated _after_ boot. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
```mermaid | ||||||||||||||||||||||||||||||
graph LR | ||||||||||||||||||||||||||||||
A[User's Browser]:::actor | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
subgraph S1["Before — frontend orchestrates boot"] | ||||||||||||||||||||||||||||||
A --> FFE[app-frontend] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q1[GET /texts] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q2[GET /app-metadata] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q3[GET /profile/party] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q4[GET /instances?active=true] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q5[GET /orgs] | ||||||||||||||||||||||||||||||
FFE -->|fetch on mount| Q6[GET /ui-config] | ||||||||||||||||||||||||||||||
FFE -.-> FX[Complex provider/effect graph] | ||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
subgraph S2["After — backend assembles initial payload"] | ||||||||||||||||||||||||||||||
A --> BE[app-backend HomeController] | ||||||||||||||||||||||||||||||
BE -->|orchestrates| P1[(Platform APIs)] | ||||||||||||||||||||||||||||||
P1 --> BE | ||||||||||||||||||||||||||||||
BE -->|Bootstrap HTML + AppData + InitialInstanceData| A | ||||||||||||||||||||||||||||||
A --> FFE2[app-frontend hydrate & render] | ||||||||||||||||||||||||||||||
FFE2 -->|reads global variable| QC[global state variables] | ||||||||||||||||||||||||||||||
FFE2 -->|targeted fetch on demand| P2[(Platform APIs)] | ||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
classDef actor fill:#eef,stroke:#77f,color:#000 | ||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Rationale | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- **Performance:** Fewer requests and no waterfall; better TTFB/TTI by shipping what the app needs up front. | ||||||||||||||||||||||||||||||
- **Simplicity:** One place orchestrates initial data; frontend boot becomes deterministic. | ||||||||||||||||||||||||||||||
- **Reliability:** Backend can decide what to load based on route/instance/party and enforce pagination/limits. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Considered Options | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
1. **Status quo (frontend-orchestrated queries).** | ||||||||||||||||||||||||||||||
Rejected: retains complexity and network overhead. | ||||||||||||||||||||||||||||||
2. **Bulk endpoints.** | ||||||||||||||||||||||||||||||
Rejected: increases backend complexity without clear benefit vs app-backend controller. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Technical Notes (non-normative) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- **Bootstrap contract** | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
```html | ||||||||||||||||||||||||||||||
<script | ||||||||||||||||||||||||||||||
id="__appdata" | ||||||||||||||||||||||||||||||
type="application/json" | ||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||
{ ... AppData ... } | ||||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||||
<script | ||||||||||||||||||||||||||||||
id="__initialInstanceData" | ||||||||||||||||||||||||||||||
type="application/json" | ||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||
{ ... } <!-- present only when opening an instance --> | ||||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||||
martinothamar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
<script> | ||||||||||||||||||||||||||||||
window.AltinnAppData = JSON.parse(document.getElementById('__appdata').textContent); | ||||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||||
<script> | ||||||||||||||||||||||||||||||
window.AltinnInitialInstanceData = JSON.parse( | ||||||||||||||||||||||||||||||
document.getElementById('__initialInstanceData')?.textContent || 'null', | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- **Routing:** | ||||||||||||||||||||||||||||||
- Backend maps `/` `/instances/:id/*` `/tasks/:taskId` etc. to a single `HomeController` action that embeds the correct initial payload and serves static assets. | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it actually possible to make this work with our current API routes? E.g. we have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! The frontend route is singular; |
||||||||||||||||||||||||||||||
- Legacy `/#/...` routes: frontend redirects to updated routes. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- **Pagination upgrades:** | ||||||||||||||||||||||||||||||
- Add pagination on _active instances_ and _parties_ APIs as specified in tasks. Cursor-based preferred. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Consequences | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
**Positive** | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Fewer network round trips at boot; smaller provider graph in UI. | ||||||||||||||||||||||||||||||
- Clearer ownership boundaries: backend decides _what_; frontend renders _how_. | ||||||||||||||||||||||||||||||
- Easier to test: snapshot input JSON → deterministic initial UI. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
**Negative / Trade-offs** | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Slightly more logic in backend (must maintain schema for `AppData`/`InitialInstanceData`). | ||||||||||||||||||||||||||||||
- Coordinated deploys required when the bootstrap contract changes. | ||||||||||||||||||||||||||||||
- Initial HTML grows; ensure gzip/brotli and avoid embedding large option lists—keep those via APIs. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Security & Compliance | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Ensure embedded data contains only what the current principal may see. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Rollout Plan | ||||||||||||||||||||||||||||||
Comment on lines
+124
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden security: escaping, CSP, and caching directives for embedded JSON. Embedding instance/app data in HTML has XSS and caching risks. Document the requirements here. ## Security & Compliance
@@
-- Ensure embedded data contains only what the current principal may see.
+- Ensure embedded data contains only what the current principal may see.
+- Escape sequences that could break out of the JSON/script context (e.g., `</script`), or use a safe JSON serializer that guarantees this.
+- Set a strict CSP with nonce/hashes; forbid `unsafe-inline`. Example: `script-src 'self' 'nonce-…'`.
+- Serve bootstrap responses with `Cache-Control: private, no-store` (at minimum for instance-specific payloads) and `Vary: Cookie, Authorization` to avoid proxy/CDN leaks.
+- Set `X-Content-Type-Options: nosniff`. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
1**Route migration:** enable path-based routing; ship redirects for legacy hash URLs; validate deep links. | ||||||||||||||||||||||||||||||
2**Delete old providers/effects** and RuleHandler/RuleConfiguration. | ||||||||||||||||||||||||||||||
3**Docs & templates:** update app templates and docs; provide migration guide for app owners. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Impacted Areas | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- `app-backend`: `HomeController`, payload assemblers, auth. | ||||||||||||||||||||||||||||||
- `app-frontend`: routing, bootstrapping, removal of nested providers. | ||||||||||||||||||||||||||||||
- APIs: instances list, parties list (add pagination). | ||||||||||||||||||||||||||||||
- `app-template`: must be updated to support new loading. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Open Questions | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Which option sources (datalists) are always API-fetched vs allowed to be embedded for tiny lists? (Epic notes say datalist+options stay API. Confirm.) | ||||||||||||||||||||||||||||||
- Error UX if embedded payload fails to parse (serve minimal shell with error boundary?). | ||||||||||||||||||||||||||||||
- What consequences does removing index.cshtml have and how should we support existing modifications that apps have implemented? | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Acceptance Criteria | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Boot of stateless and stateful apps requires ≤ 2 requests (HTML + assets) before first interactive paint (excluding datalist/options APIs). | ||||||||||||||||||||||||||||||
- No client refetch for data already embedded at boot. | ||||||||||||||||||||||||||||||
- Path URLs work for all previously supported deep links; legacy hashes redirect. | ||||||||||||||||||||||||||||||
- Parties/instances APIs expose tested pagination; UI consumes it. | ||||||||||||||||||||||||||||||
- Update RuleHandler/RuleConfiguration to mention that this functionality will be removed in future versions. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Comment on lines
+153
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve inconsistency: removal now vs. future removal. Decision says remove RuleHandler/RuleConfiguration “in this major”, but acceptance criteria says “mention will be removed in future versions”. Align the plan. -- Update RuleHandler/RuleConfiguration to mention that this functionality will be removed in future versions.
+- Remove RuleHandler/RuleConfiguration in this major and provide migration notes and deprecation notice in release docs. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
**Sources:** Epic details and tasks from Altinn/altinn-studio issue #16309. ([GitHub][1]) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
[1]: https://github.com/Altinn/altinn-studio/issues/16309 'Move most initial data loading to app-backend · Issue #16309 · Altinn/altinn-studio · GitHub' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we also make a point of measuring the baseline, so that we can also measure the impact of these changes? 100% agreed on the principles of this, but would be even better if we had some data to point to. If we feel like auomated testing is out of scope, we could just include some manual measurements as text here based on a couple of waterfalls that are problematic/that we expect to improve. Examples
Even if it were just screenshots embedded into the markdown here it would help to motivate decisions and clarify which parts of the waterfalls are most important (or even not covered by the current proposal).
In the future we should have tests as code as well that can be ran to verify changes in performance over time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you mention
TTFB/TTI
, could include those perhaps