https://github.com/robinbraemer/sveltekit-static-to-remote
๐ฑ๐ Demo: Call SvelteKit remote functions from a static frontend to a separate backend deployment. Perfect for CDNs, mobile apps (Tauri/Capacitor), and serverless architectures.
https://github.com/robinbraemer/sveltekit-static-to-remote
capacitor cdn cross-deployment mobile-apps monorepo pnpm remote-functions serverless service-worker static-site sveltekit tauri typescript
Last synced: about 2 months ago
JSON representation
๐ฑ๐ Demo: Call SvelteKit remote functions from a static frontend to a separate backend deployment. Perfect for CDNs, mobile apps (Tauri/Capacitor), and serverless architectures.
- Host: GitHub
- URL: https://github.com/robinbraemer/sveltekit-static-to-remote
- Owner: robinbraemer
- Created: 2025-09-09T11:12:35.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-09-11T10:12:19.000Z (9 months ago)
- Last Synced: 2025-10-18T07:57:51.191Z (8 months ago)
- Topics: capacitor, cdn, cross-deployment, mobile-apps, monorepo, pnpm, remote-functions, serverless, service-worker, static-site, sveltekit, tauri, typescript
- Language: TypeScript
- Size: 129 KB
- Stars: 44
- Watchers: 0
- Forks: 2
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# ๐ SvelteKit Static-to-Remote
**Call SvelteKit remote functions from a static frontend to a separate backend deployment**
[](https://github.com/robinbraemer/sveltekit-static-to-remote)
[](https://svelte.dev/docs/kit/remote-functions)
[](#mobile-apps)
[](#tech-stack)
https://github.com/user-attachments/assets/dae1146a-9391-4ab9-911e-b951b2fe7942
In this demo, the static frontend example is deployed on Surge CDN, while the backend is running locally and exposed through a Cloudflare tunnel. You can see that the remote functions are calling the backend URL instead of the frontend URL.
---
## โก TLDR
**๐ฏ Problem**: SvelteKit remote functions only work within the same deployment, but you want static frontend + separate backend.
**๐ก Solution**: Service worker magic!
1. **Same file paths** in both apps โ **identical endpoint hashes**
2. **Service worker** intercepts `/_app/remote/[HASH]/call` โ redirects to backend
3. **Backend CORS** handles cross-origin requests securely
4. **Result**: Seamless remote function calls across deployments! โจ
**Perfect for**: Static sites โข Mobile apps (Tauri/Capacitor) โข CDN deployments โข Serverless architectures
---
## ๐ Quick Start
```bash
# Clone and install
git clone https://github.com/robinbraemer/sveltekit-static-to-remote.git
cd sveltekit-static-to-remote && pnpm install
# Start backend (terminal 1)
cd apps/backend && pnpm dev # http://localhost:5174
# Build and serve frontend (terminal 2)
cd apps/frontend && pnpm build && pnpx serve build # http://localhost:3000
```
**๐งช Test**: Open โ try the form, buttons, text converter โ functions execute on backend!
---
## ๐ง How It Works
**The Secret**: SvelteKit generates remote function hashes based on **file paths**, not code.
1. **Same file paths** (`src/lib/all.remote.ts`) in both apps โ **identical hashes**
2. **Service worker** ([`service-worker.ts`](apps/frontend/src/service-worker.ts)) intercepts `/_app/remote/[HASH]/call`
3. **Redirects** to backend server with custom `X-SvelteKit-Remote` header
4. **Backend CORS** ([`hooks.server.ts`](apps/backend/src/hooks.server.ts)) allows cross-origin calls
5. **Backend** recognizes hash โ executes function โ returns result
**This means**: Static app calls non-existent endpoints โ service worker intercepts โ backend executes โ seamless API!
> **Note**: Query, form, and command functions work perfectly. **Prerender functions currently fail cross-origin** due to service worker unable to reach backend during static serving.
---
## ๐ Implementation
**Core Files:**
- ๐ **Service Worker**: [`service-worker.ts`](apps/frontend/src/service-worker.ts) - Intercepts and forwards remote calls
- ๐ **CORS Handler**: [`hooks.server.ts`](apps/backend/src/hooks.server.ts) - Handles cross-origin requests securely
- โก **Remote Functions**: [`api.ts`](apps/backend/src/lib/server/api.ts) - Query, form, command, prerender implementations
**Key Pattern**: Use **identical file paths** (`src/lib/all.remote.ts`) in both apps to ensure matching hashes.
> **๐ Note on Remote File Structure**: We use a single `all.remote.ts` file to re-export all remote functions for simplicity. You _could_ have multiple remote files like `lib/users.remote.ts`, `lib/orders.remote.ts`, etc., but you'd need to ensure each file exists at the **exact same path** in both apps (since the hash is based on file path). Using a single `all.remote.ts` file simplifies maintenance and reduces the chance of path mismatches between frontend and backend.
---
## ๐ฏ Demo: All 4 Remote Function Types
### ๐ **Query** - Dynamic Data
- **Purpose**: Real-time backend data fetching
- **Example**: Text converter with instant transformation
- **Features**: Type-safe responses, reactive loading states
### ๐ **Form** - Progressive Enhancement
- **Purpose**: Type-safe form submissions
- **Example**: Contact form with validation
- **Features**: Works without JS, built-in reactive states (`.pending`, `.result`)
### โก **Command** - Fire & Forget
- **Purpose**: Server actions without return data
- **Example**: Activity logging, analytics tracking
- **Features**: Instant feedback, no response data
### ๐ **Prerender** - Build-time Static
- **Purpose**: Static data generated at build time
- **Example**: App info, stats, configuration
- **Status**: โ **Currently unsupported cross-origin** (service worker cannot reach backend during static serving)
---
## โ ๏ธ Known Limitation: Prerender Cross-Origin
**What Works**: โ
Prerender functions ARE called during backend build time
**What Fails**: โ Static app attempts runtime calls to backend but service worker cannot establish connection
**Root Cause**: Prerender functions bypass static cache and attempt cross-origin calls at runtime. Service worker intercepts but cannot reach backend server during static serving.
**๐ PRs Welcome!** Help implement proper prerender cache serving or fix cross-origin prerender calls!
---
## ๐ Production Testing (No Signup Required)
๐ Test cross-origin functionality without signup using Surge.sh + Cloudflare Tunnel
### **๐ฆ Prerequisites**
```bash
# Install tools (no accounts needed)
pnpm install -g surge
# Install cloudflared: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
```
### **๐ฏ Quick Deployment Test**
Step 1: Configure Backend for Production
```bash
# 1. Add your test domain to CORS
# Edit: apps/backend/src/hooks.server.ts
const ALLOWED_ORIGINS = [
// ... existing origins
'https://your-test-domain.surge.sh', // Add your chosen domain
];
# 2. Allow Cloudflare tunnel domains
# Edit: apps/backend/vite.config.js
export default {
server: {
allowedHosts: [
'.trycloudflare.com', // Allow any trycloudflare subdomain
]
}
};
```
**๐ก Enables curl testing**: `curl -i https://xxx.trycloudflare.com/_app/version.json`
Step 2: Start Backend + Tunnel
```bash
# Terminal 1: Backend with public tunnel
cd apps/backend
pnpm build && node build/index.js &
cloudflared tunnel --url http://localhost:5174
# ๐ Copy the https://xxx.trycloudflare.com URL (appears in terminal)
```
Step 3: Deploy Frontend
```bash
# Terminal 2: Configure & deploy frontend
cd apps/frontend
echo 'PUBLIC_BACKEND_HOST="xxx.trycloudflare.com"' > .env
echo 'PUBLIC_BACKEND_INSECURE="false"' >> .env
pnpm build
surge ./build
# ๐ Choose domain: your-test-domain.surge.sh
```
### **๐งช Expected Results**
- **โ
Query/Form/Command**: Cross-origin requests working
- **โ Prerender**: CORS failures (documented limitation)
- **โ
Service Worker**: Console logs show interception
### **๐ง Quick Validation with Curl**
```bash
# Test OPTIONS preflight (should return 204 with CORS headers)
curl -i -H "Origin: https://your-test.surge.sh" \
-X OPTIONS \
https://xxx.trycloudflare.com/_app/remote/13eoo5e/toUpper
# Test backend health (should return 200 OK)
curl -i https://xxx.trycloudflare.com/_app/version.json
```
**๐ฏ This validates real production cross-origin scenarios without browser testing!**
---
## ๐งช Testing
**Comprehensive test suite with automated validation:**
```bash
# Run all tests
pnpm test:all # Builds + API tests + E2E tests
# Individual test suites
pnpm test # API tests (Vitest)
pnpm e2e # Browser tests (Playwright)
```
### ๐ **Test Coverage:**
- **โ
API Tests** (`apps/tests/src/api/`):
- CORS validation for all remote function types
- Cross-origin request/response verification
- Backend server integration testing
- **โ
E2E Tests** (`apps/tests/src/e2e/`):
- **3 browsers tested**: Chrome, Firefox, Safari
- Service worker functionality validation
- Complete user interaction flows
- Network request tracking and analysis
- **โ
Infrastructure Tests**:
- Automated build and serve setup
- Robust timeout and cleanup handling
- Process management (no hanging tests)
### ๐ฏ **Test Results:**
- **API Tests**: 3/4 pass (prerender limitation documented)
- **E2E Tests**: 3/3 browsers pass
- **Service Worker**: โ
Intercepts all remote calls correctly
- **Cross-Origin**: โ
Query/Form/Command work perfectly
**๐ฌ Scientific Validation**: E2E tests discovered and corrected false assumptions about prerender caching behavior.
---
## ๐ง Configuration
### Frontend Service Worker
**File**: [`apps/frontend/src/service-worker.ts`](apps/frontend/src/service-worker.ts)
```typescript
const productionHost = 'api.yourdomain.com'; // Your backend domain
const productionSecure = true; // true for HTTPS
```
### Backend CORS Setup
**File**: [`apps/backend/src/hooks.server.ts`](apps/backend/src/hooks.server.ts)
```typescript
const ALLOWED_ORIGINS = [
'http://localhost:5173', // frontend dev
'http://localhost:3000', // frontend serve
'https://yourdomain.com', // production frontend
'capacitor://localhost', // Capacitor iOS
'http://localhost', // Capacitor Android
'tauri://localhost', // Tauri desktop
];
```
### Mobile Apps (Tauri/Capacitor)
**Additional setup for mobile apps:**
- Add mobile origins to `ALLOWED_ORIGINS` (see above)
- Set `productionHost` to your API server domain
- Use HTTPS in production (`productionSecure: true`)
- Mobile apps cache static build but call live backend functions
---
## ๐ Deployment
**๐ฆ Frontend (Static)**
- Vercel, Netlify, GitHub Pages โ Deploy `build/` folder
- Any CDN or static hosting service
- Mobile frameworks: Tauri (desktop), Capacitor (iOS/Android), Electron
**๐ฅ๏ธ Backend (Server)**
- Railway, Fly.io, VPS โ Deploy with Node.js
- Vercel Functions, Netlify Functions โ Deploy as serverless
- Any container or traditional server
**๐ฑ Mobile Advantage**: Tauri and Capacitor require static builds since they bundle your web app into native containers. This technique lets you keep heavy backend logic on servers while maintaining elegant remote function APIs!
---
## ๐ ๏ธ Troubleshooting
- **Service worker not working?** DevTools โ Application โ Service Workers โ Update + reload
- **CORS errors?** Add your frontend origin to `ALLOWED_ORIGINS` in backend hook
- **JSON parsing errors?** Fixed with improved service worker error handling
- **Prerender fails to load?** Expected - currently unsupported cross-origin (use query functions instead)
---
## ๐ Benefits
- โ
**Elegant API**: Use SvelteKit's remote functions instead of manual fetch
- โ
**Type Safety**: Full TypeScript support across frontend/backend
- โ
**Separate Deployments**: Frontend and backend deploy independently
- โ
**Static Hosting**: CDN/GitHub Pages compatible
- โ
**Mobile Ready**: Perfect for Tauri/Capacitor apps
- โ **Prerender Functions**: Currently unsupported cross-origin (3 out of 4 function types work)
---
## ๐ Technical Deep Dive
**The Hash Secret**: SvelteKit generates endpoint hashes based on **file paths**, not code content.
```javascript
// SvelteKit source (simplified):
remotes.push({
hash: hash(filePath), // Hash of file path
file: filePath, // e.g., "src/lib/all.remote.ts"
});
```
**Why This Works**:
- Both apps use `src/lib/all.remote.ts` โ same hash โ same endpoint
- Frontend calls `/_app/remote/13eoo5e/call` (doesn't exist locally)
- Service worker intercepts โ forwards to `backend.com/_app/remote/13eoo5e/call`
- Backend recognizes hash `13eoo5e` โ executes function โ returns result
**Service Worker Flow**:
1. Clone original request to preserve body streams
2. Add `X-SvelteKit-Remote` header for backend detection
3. Forward with preserved cookies, referrer, and metadata
4. Handle POST body buffering to avoid stream consumption issues
---
## ๐ ๏ธ Tech Stack
- **SvelteKit** - Remote functions framework
- **TypeScript** - Type safety across deployments
- **Service Workers** - Request interception and forwarding
- **Zod** - Request/response validation
- **PNPM** - Efficient monorepo management
---
## ๐ References
- [SvelteKit Remote Functions](https://svelte.dev/docs/kit/remote-functions)
- [Service Workers Documentation](https://svelte.dev/docs/kit/service-workers)
- [Remote Functions Hashing Source](https://github.com/sveltejs/kit/blob/main/packages/kit/src/core/sync/create_manifest_data/index.js)
- [Hash Function Implementation](https://github.com/sveltejs/kit/blob/main/packages/kit/src/utils/hash.js)
---
**Made with โค๏ธ by [Robin Braemer](https://github.com/robinbraemer)**
_Building bridges between static frontends and dynamic backends_
[โญ Star](https://github.com/robinbraemer/sveltekit-static-to-remote) โข [๐ด Fork](https://github.com/robinbraemer/sveltekit-static-to-remote/fork) โข [๐ฌ Discuss](https://github.com/robinbraemer/sveltekit-static-to-remote/discussions)