Skip to content

Commit c9d27cb

Browse files
authored
Merge pull request #319 from objectstack-ai/copilot/optimize-static-file-paths
2 parents 4134298 + 2dd93ba commit c9d27cb

5 files changed

Lines changed: 94 additions & 2 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@ packages/server/objectstack.config.js
3030
*.tgz
3131
package/
3232

33+
# SPA static assets copied during Vercel build (build artifacts, not source)
34+
public/console/
35+
public/_studio/
36+
3337
.objectstack

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Changed
11+
- **Optimize static file access paths — serve SPA assets directly from Vercel CDN** — Console
12+
and Studio SPA static assets (JS, CSS, fonts, images) are now copied to `public/console/` and
13+
`public/_studio/` during the Vercel build step. Vercel's filesystem-first routing serves these
14+
files directly from its CDN edge network, bypassing the `api/[[...route]]` serverless function
15+
entirely. This eliminates unnecessary cold-start latency for asset requests, enables browser
16+
and CDN caching with `Cache-Control: public, max-age=31536000, immutable` headers for
17+
content-hashed assets, and reduces serverless function invocations. The API handler retains
18+
its static SPA plugin for non-Vercel deployments (Docker, local dev) and SPA HTML fallback.
19+
- Updated `scripts/build-vercel.sh` to copy SPA dist assets to `public/` after package builds
20+
- Added `headers` configuration in `vercel.json` for long-lived cache on asset paths
21+
- Updated `.gitignore` to exclude `public/console/` and `public/_studio/` (build artifacts)
1122
- **Vercel deployment — switched from InMemoryDriver to TursoDriver** — The Vercel serverless
1223
handler (`api/[[...route]].ts`) now uses `@objectstack/driver-turso` (TursoDriver) instead of
1324
`@objectstack/driver-memory` (InMemoryDriver). In production, set `TURSO_DATABASE_URL` and

docs/DEPLOYMENT.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,28 @@ Build pipeline:
289289
| Field | Value | Purpose |
290290
|-------|-------|---------|
291291
| `installCommand` | `pnpm install` | Installs all workspace dependencies |
292-
| `buildCommand` | `bash scripts/build-vercel.sh` | Runs the Vercel build script that builds core, patches console plugin, then compiles all business plugins |
292+
| `buildCommand` | `bash scripts/build-vercel.sh` | Runs the Vercel build script that builds core, patches console plugin, compiles all business plugins, and copies SPA static assets to `public/` |
293293
| `functions.memory` | `1024` MB | Memory allocated to the serverless function |
294294
| `functions.maxDuration` | `60` s | Maximum execution time per request (Pro plan) |
295295
| `functions.includeFiles` | `{packages/*/dist,node_modules/@object-ui/console/dist,node_modules/@objectstack/*/dist,node_modules/@libsql,node_modules/better-sqlite3,node_modules/@opentelemetry/api}/**` | Bundles business plugin dist/, all @objectstack packages (including Auth, Studio, TursoDriver), libSQL/better-sqlite3 native deps, and OpenTelemetry with the function |
296-
| `rewrites` | `/(.*) → /api/[[...route]]` | Routes all requests to the catch-all handler |
296+
| `headers` | `/console/assets/*`, `/_studio/assets/*` | Sets `Cache-Control: public, max-age=31536000, immutable` on content-hashed static assets |
297+
| `rewrites` | `/(.*) → /api/[[...route]]` | Routes non-static requests to the catch-all handler (static files in `public/` are served by Vercel's CDN before rewrites are evaluated) |
298+
299+
### Static Asset Optimization
300+
301+
SPA static assets (JS, CSS, fonts, images) are served directly from Vercel's CDN edge network
302+
instead of routing through the serverless function:
303+
304+
- **Build step**: `scripts/build-vercel.sh` copies Console assets to `public/console/assets/`
305+
and Studio assets to `public/_studio/assets/` after building all packages.
306+
- **Vercel routing order**: redirects → headers → **filesystem** → rewrites. Files in `public/`
307+
(the output directory) are matched at the filesystem step, **before** rewrites, so they bypass
308+
the `api/[[...route]]` serverless function entirely.
309+
- **Cache headers**: Content-hashed assets (e.g., `index-DDpLaQOV.js`) get
310+
`Cache-Control: public, max-age=31536000, immutable` for optimal CDN and browser caching.
311+
- **SPA fallback**: Only `index.html` and client-side routes still go through the API handler.
312+
The static SPA plugin in the serverless function handles these requests, plus serves as the
313+
fallback for non-Vercel deployments (Docker, local dev).
297314

298315
### Architecture Details
299316

scripts/build-vercel.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# 1. Build the core package first (other packages depend on it)
99
# 2. Patch the console plugin (dereference pnpm symlinks)
1010
# 3. Build all business packages in parallel
11+
# 4. Copy SPA static assets to public/ for direct serving by Vercel's CDN
1112
#
1213
# Usage (called automatically by Vercel via vercel.json):
1314
# bash scripts/build-vercel.sh
@@ -37,4 +38,49 @@ pnpm --filter @hotcrm/ai \
3738
--filter @hotcrm/financial-services \
3839
build
3940

41+
# ---------------------------------------------------------------------------
42+
# Copy SPA static assets to public/ so Vercel serves them directly from CDN
43+
# instead of routing through the serverless function.
44+
#
45+
# Vercel's routing order: redirects → headers → filesystem → rewrites
46+
# Files in public/ (the outputDirectory) are matched at the "filesystem"
47+
# step, BEFORE rewrites, so they bypass the API handler entirely.
48+
#
49+
# Only asset files (JS, CSS, fonts, images) are copied — NOT index.html.
50+
# SPA entry points must still go through the API handler for proper
51+
# client-side routing fallback.
52+
# ---------------------------------------------------------------------------
53+
54+
echo "▸ Copying SPA static assets to public/ for direct CDN serving..."
55+
56+
# Console SPA (@object-ui/console)
57+
CONSOLE_DIST="node_modules/@object-ui/console/dist"
58+
if [ -d "$CONSOLE_DIST/assets" ]; then
59+
mkdir -p public/console/assets
60+
cp -r "$CONSOLE_DIST/assets/"* public/console/assets/
61+
echo " ✓ Console assets copied to public/console/assets/"
62+
# Copy manifest.json if present (referenced by <link rel="manifest">)
63+
if [ -f "$CONSOLE_DIST/manifest.json" ]; then
64+
cp "$CONSOLE_DIST/manifest.json" public/console/manifest.json
65+
echo " ✓ Console manifest.json copied"
66+
fi
67+
else
68+
echo " ⚠ Console dist/assets not found — skipping"
69+
fi
70+
71+
# Studio SPA (@objectstack/studio)
72+
STUDIO_DIST="node_modules/@objectstack/studio/dist"
73+
if [ -d "$STUDIO_DIST/assets" ]; then
74+
mkdir -p public/_studio/assets
75+
cp -r "$STUDIO_DIST/assets/"* public/_studio/assets/
76+
echo " ✓ Studio assets copied to public/_studio/assets/"
77+
# Copy vite.svg favicon if present
78+
if [ -f "$STUDIO_DIST/vite.svg" ]; then
79+
cp "$STUDIO_DIST/vite.svg" public/_studio/vite.svg
80+
echo " ✓ Studio vite.svg copied"
81+
fi
82+
else
83+
echo " ⚠ Studio dist/assets not found — skipping"
84+
fi
85+
4086
echo "✓ Vercel build complete."

vercel.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@
1010
"includeFiles": "{packages/*/dist,node_modules/@object-ui/console/dist,node_modules/@objectstack/*/dist,node_modules/@libsql,node_modules/better-sqlite3,node_modules/@opentelemetry/api}/**"
1111
}
1212
},
13+
"headers": [
14+
{
15+
"source": "/console/assets/(.*)",
16+
"headers": [
17+
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
18+
]
19+
},
20+
{
21+
"source": "/_studio/assets/(.*)",
22+
"headers": [
23+
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
24+
]
25+
}
26+
],
1327
"rewrites": [
1428
{ "source": "/api/:path*", "destination": "/api/[[...route]]" },
1529
{ "source": "/(.*)", "destination": "/api/[[...route]]" }

0 commit comments

Comments
 (0)