ARFC-1002: Selective SPA Fallback for Subproject Static Assets
Status
| Implemented | Draft Date: 2026-02-20 | Last Call Date: 2026-02-27 | Publication Date: 2026-03-06 | Version: 1.2 |
Abstract
This RFC proposes a selective Single Page Application (SPA) fallback mechanism for Vite-built subprojects hosted under /apps/{appName}/. The current configuration returns the subproject index.html for all requests under the subproject prefix, including requests for static assets and Vite-generated code-split JavaScript chunks. This breaks runtime module loading because chunk requests receive HTML instead of JavaScript.
The proposed solution introduces edge-side request rewriting that only falls back to the appropriate index.html for route-like requests, while allowing file-like requests (e.g., *.js, *.css, images, fonts) to be served directly from storage (S3) or return a true 404. This avoids changing Vite’s asset location behavior (which is base-path driven) and restores correct delivery of static assets and code-split chunks. oai_citation:0‡Ayode Institute
Table of Contents
- Introduction
- Motivation
- Problem Statement
- Requirements
- Proposed Solution
- Implementation Notes
- Error Handling
- Security Considerations
- Backward Compatibility
- Deployment Plan
- Examples
- References
1. Introduction
Codermerlin Academy hosts multiple Vite-built frontend subprojects under a static namespace, typically:
/apps/{appName}/
Vite’s production build rewrites all asset paths relative to the configured base option, and treats code-split JavaScript chunks as static assets that must be retrievable from the same base path. oai_citation:1‡v3.vitejs.dev
This RFC defines how the platform must serve those static outputs so that:
- static files are fetched as files, and
- SPA routes fall back to the appropriate
index.htmlonly when intended.
The ARFC structure and required sections follow the established template used by ARFC-1001. oai_citation:2‡Ayode Institute
2. Motivation
Current Challenges
- Vite base-path coupling
- Vite bundles assets and dynamic-import chunk paths using the configured base path; there is no supported “assets live elsewhere” override that fits this deployment model without re-architecting the build output layout. oai_citation:3‡v3.vitejs.dev
- SPA fallback applied too broadly
- The current routing returns
{appName}/index.htmlfor requests that are actually static asset fetches (e.g.,assets/*.js), causing chunk loads to return HTML.
- The current routing returns
- Operational fragility
- Changes in routing or caching can silently break subprojects when chunk URLs begin resolving to
index.html.
- Changes in routing or caching can silently break subprojects when chunk URLs begin resolving to
Desired Outcome
-
SPA navigations like:
/apps/foo/settingsreturn:
/apps/foo/index.html -
Static asset requests like:
/apps/foo/assets/index-ABC123.jsreturn the actual object (or a true 404), never
index.html.
3. Problem Statement
Under the current configuration, any request to a subproject path is routed to a function/origin behavior that returns the subproject index.html. As a result, requests for:
- Vite-generated static assets (CSS/images/fonts), and
- Vite-generated code-split JavaScript chunk files
receive HTML instead of the requested file content, breaking application execution.
This is a serving-layer problem (edge/origin routing), not a frontend bundling problem: Vite’s behavior is correct for a nested base path deployment. oai_citation:4‡v3.vitejs.dev
4. Requirements
- Correctness
- Any request that targets an existing static object MUST be served as that object.
- SPA fallback MUST NOT be applied to file-like requests.
- Per-subproject isolation
- Each subproject MUST fall back to its own
index.html, not the main application’s index.
- Each subproject MUST fall back to its own
- Low latency
- Solution SHOULD be implementable at the edge (CloudFront Function preferred) without per-request origin probes.
- No changes required to Vite build outputs
- Subprojects MUST continue to use Vite
base="/apps/{appName}/"semantics.
- Subprojects MUST continue to use Vite
5. Proposed Solution
Overview
Implement selective SPA fallback using a deterministic rule at the edge:
- If the request is route-like (no file extension, and not an explicit “file request”), rewrite to the correct subproject
index.html. - If the request is file-like (has an extension such as
.js,.css,.svg,.woff2, etc.), do not rewrite; allow normal object retrieval and normal 404 behavior.
This approach aligns with CloudFront’s request processing model, where edge logic can rewrite the request URI before it reaches the origin. oai_citation:5‡AWS Documentation
5.1. Routing Rules
A request is considered file-like if the final path segment contains a period (.) followed by a non-empty extension:
Examples (file-like):
/apps/foo/assets/index-ABC123.js/apps/foo/assets/styles-XYZ.css/apps/foo/favicon.ico
A request is considered route-like if:
- it does not contain a file extension in the last segment, OR
- it ends in
/(directory-like navigation)
Examples (route-like):
/apps/foo//apps/foo/settings/apps/foo/users/123
Rule:
- Route-like ⇒ rewrite to
/apps/{appName}/index.html - File-like ⇒ do not rewrite
5.2. Per-Subproject Fallback Target
For any request under:
/apps/{appName}/...
the fallback MUST be:
/apps/{appName}/index.html
This ensures:
- subproject routes resolve within the correct SPA bundle,
- main application fallback behavior remains independent.
6. Implementation Notes
6.1. CloudFront Function (Viewer Request) Rewrite
Attach a CloudFront Function to the viewer-request event for the static distribution behavior that serves /apps/*.
The function:
- Detects whether the path matches
/apps/{appName}/... - Determines file-like vs route-like
- Rewrites only route-like requests to the corresponding
index.html
This avoids CloudFront “custom error response” patterns that can unintentionally rewrite asset 404s into 200 responses with HTML.
6.2. Why Not “Try S3 Then Fallback” at Runtime
A strategy of:
- forwarding the original request to S3,
- detecting a 404,
- then returning
index.html
is operationally expensive at the edge, may require origin-response logic, and tends to introduce caching pitfalls (e.g., caching fallback HTML under an asset key). The deterministic rewrite rule avoids origin probing and produces stable caching semantics.
7. Error Handling
- Static asset not found
- If the request is file-like and the object does not exist, the response MUST remain a true not-found (e.g., 404), not
index.html.
- If the request is file-like and the object does not exist, the response MUST remain a true not-found (e.g., 404), not
- Subproject not found
- If the request is under
/apps/{appName}/but the subproject does not exist (missingindex.html), the response SHOULD be a true not-found.
- If the request is under
- Main application fallback
- Existing main-application fallback behavior MUST remain unchanged and MUST NOT interfere with subproject behaviors.
8. Security Considerations
- No privilege escalation
- This RFC only changes request rewriting for static content paths and does not alter authorization or identity semantics.
- Cache correctness
- Rewriting must be deterministic and only applied to route-like requests to prevent cache poisoning (e.g., storing HTML under a
.jsURL).
- Rewriting must be deterministic and only applied to route-like requests to prevent cache poisoning (e.g., storing HTML under a
- Content-type integrity
- Ensuring
.jsrequests never return HTML prevents browsers from executing incorrect content and reduces the risk of confusing client-side security controls.
- Ensuring
9. Backward Compatibility
- Existing direct asset URLs remain valid and will begin returning correct objects instead of
index.html. - Existing SPA deep links will continue to work due to route-like rewriting.
- This change is additive and does not require changes to existing Vite builds. oai_citation:6‡v3.vitejs.dev
10. Deployment Plan
- Remove or narrow any “global 404/403 ⇒ index.html” mappings that apply to
/apps/*and would convert missing assets into HTML. - Add CloudFront Function for viewer-request on the behavior that matches
/apps/*. - Validate
- Fetch known chunk URLs and verify
Content-Type: application/javascript. - Fetch known deep links and verify
index.htmlis served.
- Fetch known chunk URLs and verify
- Rollout
- Deploy to development, then staging, then production.
- Observability
- Track 404 rates for static assets (expected to rise temporarily if assets were previously masked by fallback).
11. Examples
11.1. CloudFront Function (Viewer Request) Example
function isFileLike(pathname) {
// File-like if the last segment contains a dot.
// Examples: /assets/index-ABC123.js, /favicon.ico
var lastSlashIndex = pathname.lastIndexOf("/");
var lastSegment = (lastSlashIndex >= 0) ? pathname.substring(lastSlashIndex + 1) : pathname;
return lastSegment.indexOf(".") !== -1;
}
function handler(event) {
var request = event.request;
var uri = request.uri;
// Match: /apps/{appName}/...
// Capture {appName} as the segment immediately after "/apps/"
var prefix = "/apps/";
if (uri.indexOf(prefix) !== 0) {
return request;
}
var remainder = uri.substring(prefix.length); // "{appName}/..."
var firstSlash = remainder.indexOf("/");
if (firstSlash === -1) {
// Path is exactly "/apps/{appName}" (no trailing slash)
// Treat as route-like and rewrite.
request.uri = prefix + remainder + "/index.html";
return request;
}
var appName = remainder.substring(0, firstSlash);
var afterApp = remainder.substring(firstSlash); // "/..."
// If request is exactly the app root or a directory navigation, rewrite.
if (afterApp === "/" || afterApp === "") {
request.uri = prefix + appName + "/index.html";
return request;
}
// If file-like, do not rewrite.
if (isFileLike(uri)) {
return request;
}
// Route-like: rewrite to subproject index.
request.uri = prefix + appName + "/index.html";
return request;
}
12. References
Related Implementation
- Branch:
575-update-narp-dispatch-to-handle-multiple-projects - Commit:
cefa0f29- [FIX] Route NARP project paths to app index - File Modified:
dynamic-sources/templates/codermerlin-academy/purple/distributor/functions/partitionDirector/index.js.src
Infrastructure Files
| File | Purpose |
|---|---|
partition/static/template.yaml |
Per-color CloudFront distribution with CustomErrorResponses (lines 321-329) |
purple/distributor/template.yaml |
Main distributor CloudFront with Lambda@Edge associations |
purple/distributor/functions/partitionDirector/index.js.src |
Lambda@Edge origin-request function with NARP routing |
External References
- Vite Build Configuration - Base path and asset handling
- CloudFront Request/Response Behavior - Edge processing model
- ARFC-1001 - Related RFC template reference