@kortix/sandbox 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kortix-master/src/index.ts +1 -1
- package/kortix-master/src/routes/update.ts +23 -66
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +19 -19
- package/opencode/skills/KORTIX-deploy/SKILL.md +438 -0
- package/opencode/skills/KORTIX-deploy/references/freestyle-api.md +279 -0
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ app.get('/kortix/health', async (c) => {
|
|
|
28
28
|
version = data.version || '0.0.0'
|
|
29
29
|
}
|
|
30
30
|
} catch {}
|
|
31
|
-
return c.json({ status: 'ok', version })
|
|
31
|
+
return c.json({ status: 'ok', version, build: '0.4.4-update-test' })
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
// Update check — /kortix/update and /kortix/update/status
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { config } from '../config';
|
|
3
|
-
|
|
4
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
interface LocalVersion {
|
|
7
|
-
version: string;
|
|
8
|
-
updatedAt: string;
|
|
9
|
-
}
|
|
10
2
|
|
|
11
3
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
12
4
|
|
|
@@ -32,35 +24,17 @@ let updateInProgress = false;
|
|
|
32
24
|
|
|
33
25
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
34
26
|
|
|
35
|
-
async function readLocalVersion(): Promise<
|
|
27
|
+
async function readLocalVersion(): Promise<string> {
|
|
36
28
|
try {
|
|
37
29
|
const file = Bun.file(VERSION_FILE);
|
|
38
30
|
if (await file.exists()) {
|
|
39
|
-
|
|
31
|
+
const data = await file.json();
|
|
32
|
+
return data.version || '0.0.0';
|
|
40
33
|
}
|
|
41
34
|
} catch (e) {
|
|
42
35
|
console.error('[Update] Failed to read version file:', e);
|
|
43
36
|
}
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function fetchLatestVersion(): Promise<string | null> {
|
|
48
|
-
const url = `${config.KORTIX_API_URL}/v1/sandbox/version`;
|
|
49
|
-
try {
|
|
50
|
-
const res = await fetch(url, {
|
|
51
|
-
headers: { 'Accept': 'application/json' },
|
|
52
|
-
signal: AbortSignal.timeout(10_000),
|
|
53
|
-
});
|
|
54
|
-
if (!res.ok) {
|
|
55
|
-
console.error(`[Update] Version fetch failed: ${res.status}`);
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const data = await res.json() as { version: string };
|
|
59
|
-
return data.version;
|
|
60
|
-
} catch (e) {
|
|
61
|
-
console.error('[Update] Failed to fetch version:', e);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
37
|
+
return '0.0.0';
|
|
64
38
|
}
|
|
65
39
|
|
|
66
40
|
async function run(cmd: string): Promise<{ ok: boolean; output: string }> {
|
|
@@ -116,62 +90,45 @@ async function performUpdate(targetVersion: string): Promise<{
|
|
|
116
90
|
|
|
117
91
|
const updateRouter = new Hono();
|
|
118
92
|
|
|
119
|
-
/**
|
|
120
|
-
* GET /kortix/update/status
|
|
121
|
-
*
|
|
122
|
-
* Returns current sandbox version + latest available version.
|
|
123
|
-
* Frontend uses this to decide whether to show "Update available".
|
|
124
|
-
* Read-only — does NOT trigger any update.
|
|
125
|
-
*/
|
|
126
|
-
updateRouter.get('/status', async (c) => {
|
|
127
|
-
const local = await readLocalVersion();
|
|
128
|
-
const latest = await fetchLatestVersion();
|
|
129
|
-
|
|
130
|
-
return c.json({
|
|
131
|
-
currentVersion: local.version,
|
|
132
|
-
latestVersion: latest || 'unknown',
|
|
133
|
-
updateAvailable: latest ? local.version !== latest : false,
|
|
134
|
-
updatedAt: local.updatedAt,
|
|
135
|
-
updateInProgress,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
93
|
/**
|
|
140
94
|
* POST /kortix/update
|
|
141
95
|
*
|
|
142
|
-
* User-triggered update.
|
|
143
|
-
*
|
|
96
|
+
* User-triggered update. Frontend passes the target version.
|
|
97
|
+
* Installs the package, restarts services.
|
|
98
|
+
* Only runs when explicitly called.
|
|
99
|
+
*
|
|
100
|
+
* Body: { "version": "0.4.3" }
|
|
144
101
|
*/
|
|
145
102
|
updateRouter.post('/', async (c) => {
|
|
146
103
|
if (updateInProgress) {
|
|
147
104
|
return c.json({ error: 'Update already in progress' }, 409);
|
|
148
105
|
}
|
|
149
106
|
|
|
107
|
+
const body = await c.req.json().catch(() => ({}));
|
|
108
|
+
const targetVersion = body.version;
|
|
109
|
+
|
|
110
|
+
if (!targetVersion || typeof targetVersion !== 'string') {
|
|
111
|
+
return c.json({ error: 'Missing "version" in request body' }, 400);
|
|
112
|
+
}
|
|
113
|
+
|
|
150
114
|
updateInProgress = true;
|
|
151
115
|
try {
|
|
152
|
-
const
|
|
153
|
-
const latestVersion = await fetchLatestVersion();
|
|
154
|
-
|
|
155
|
-
if (!latestVersion) {
|
|
156
|
-
return c.json({ error: 'Could not reach version service' }, 502);
|
|
157
|
-
}
|
|
116
|
+
const currentVersion = await readLocalVersion();
|
|
158
117
|
|
|
159
|
-
if (
|
|
118
|
+
if (currentVersion === targetVersion) {
|
|
160
119
|
return c.json({
|
|
161
120
|
upToDate: true,
|
|
162
|
-
currentVersion
|
|
163
|
-
latestVersion,
|
|
121
|
+
currentVersion,
|
|
164
122
|
});
|
|
165
123
|
}
|
|
166
124
|
|
|
167
|
-
console.log(`[Update] User triggered: ${
|
|
168
|
-
const update = await performUpdate(
|
|
125
|
+
console.log(`[Update] User triggered: ${currentVersion} -> ${targetVersion}`);
|
|
126
|
+
const update = await performUpdate(targetVersion);
|
|
169
127
|
|
|
170
128
|
return c.json({
|
|
171
129
|
success: update.success,
|
|
172
|
-
previousVersion:
|
|
173
|
-
currentVersion: update.success ?
|
|
174
|
-
latestVersion,
|
|
130
|
+
previousVersion: currentVersion,
|
|
131
|
+
currentVersion: update.success ? targetVersion : currentVersion,
|
|
175
132
|
output: update.output,
|
|
176
133
|
});
|
|
177
134
|
} catch (e) {
|
|
@@ -9,8 +9,8 @@ The Kortix Cron service runs outside the sandbox as a platform service. It manag
|
|
|
9
9
|
|
|
10
10
|
## Architecture
|
|
11
11
|
|
|
12
|
-
- **Service**: `kortix-
|
|
13
|
-
- **Database**: `
|
|
12
|
+
- **Service**: `kortix-api` (Bun + Hono) running at port 8008
|
|
13
|
+
- **Database**: `kortix` schema in Supabase PostgreSQL (Drizzle ORM)
|
|
14
14
|
- **Scheduler**: Polling loop (1s tick) that checks for due triggers
|
|
15
15
|
- **Executor**: Calls OpenCode API inside sandbox to create sessions and send prompts
|
|
16
16
|
|
|
@@ -44,7 +44,7 @@ The Kortix Cron service runs outside the sandbox as a platform service. It manag
|
|
|
44
44
|
|
|
45
45
|
## API Reference
|
|
46
46
|
|
|
47
|
-
Base URL: `http://localhost:
|
|
47
|
+
Base URL: `http://localhost:8008` (local) or the deployed service URL.
|
|
48
48
|
|
|
49
49
|
All `/v1/*` endpoints require `Authorization: Bearer <supabase-jwt>`.
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ All `/v1/*` endpoints require `Authorization: Bearer <supabase-jwt>`.
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
# Register a sandbox
|
|
55
|
-
curl -X POST http://localhost:
|
|
55
|
+
curl -X POST http://localhost:8008/v1/sandboxes \
|
|
56
56
|
-H "Authorization: Bearer $TOKEN" \
|
|
57
57
|
-H "Content-Type: application/json" \
|
|
58
58
|
-d '{
|
|
@@ -62,11 +62,11 @@ curl -X POST http://localhost:8011/v1/sandboxes \
|
|
|
62
62
|
}'
|
|
63
63
|
|
|
64
64
|
# List sandboxes
|
|
65
|
-
curl http://localhost:
|
|
65
|
+
curl http://localhost:8008/v1/sandboxes \
|
|
66
66
|
-H "Authorization: Bearer $TOKEN"
|
|
67
67
|
|
|
68
68
|
# Check sandbox health
|
|
69
|
-
curl -X POST http://localhost:
|
|
69
|
+
curl -X POST http://localhost:8008/v1/sandboxes/{id}/health \
|
|
70
70
|
-H "Authorization: Bearer $TOKEN"
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ curl -X POST http://localhost:8011/v1/sandboxes/{id}/health \
|
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# Create a trigger - daily report at 9 AM UTC
|
|
77
|
-
curl -X POST http://localhost:
|
|
77
|
+
curl -X POST http://localhost:8008/v1/triggers \
|
|
78
78
|
-H "Authorization: Bearer $TOKEN" \
|
|
79
79
|
-H "Content-Type: application/json" \
|
|
80
80
|
-d '{
|
|
@@ -88,7 +88,7 @@ curl -X POST http://localhost:8011/v1/triggers \
|
|
|
88
88
|
}'
|
|
89
89
|
|
|
90
90
|
# Create a trigger - every 5 minutes health check
|
|
91
|
-
curl -X POST http://localhost:
|
|
91
|
+
curl -X POST http://localhost:8008/v1/triggers \
|
|
92
92
|
-H "Authorization: Bearer $TOKEN" \
|
|
93
93
|
-H "Content-Type: application/json" \
|
|
94
94
|
-d '{
|
|
@@ -101,23 +101,23 @@ curl -X POST http://localhost:8011/v1/triggers \
|
|
|
101
101
|
}'
|
|
102
102
|
|
|
103
103
|
# List triggers
|
|
104
|
-
curl http://localhost:
|
|
104
|
+
curl http://localhost:8008/v1/triggers \
|
|
105
105
|
-H "Authorization: Bearer $TOKEN"
|
|
106
106
|
|
|
107
107
|
# Pause a trigger
|
|
108
|
-
curl -X POST http://localhost:
|
|
108
|
+
curl -X POST http://localhost:8008/v1/triggers/{id}/pause \
|
|
109
109
|
-H "Authorization: Bearer $TOKEN"
|
|
110
110
|
|
|
111
111
|
# Resume a trigger
|
|
112
|
-
curl -X POST http://localhost:
|
|
112
|
+
curl -X POST http://localhost:8008/v1/triggers/{id}/resume \
|
|
113
113
|
-H "Authorization: Bearer $TOKEN"
|
|
114
114
|
|
|
115
115
|
# Fire a trigger immediately (manual run)
|
|
116
|
-
curl -X POST http://localhost:
|
|
116
|
+
curl -X POST http://localhost:8008/v1/triggers/{id}/run \
|
|
117
117
|
-H "Authorization: Bearer $TOKEN"
|
|
118
118
|
|
|
119
119
|
# Delete a trigger
|
|
120
|
-
curl -X DELETE http://localhost:
|
|
120
|
+
curl -X DELETE http://localhost:8008/v1/triggers/{id} \
|
|
121
121
|
-H "Authorization: Bearer $TOKEN"
|
|
122
122
|
```
|
|
123
123
|
|
|
@@ -125,30 +125,30 @@ curl -X DELETE http://localhost:8011/v1/triggers/{id} \
|
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
127
|
# List all executions
|
|
128
|
-
curl "http://localhost:
|
|
128
|
+
curl "http://localhost:8008/v1/executions?limit=20" \
|
|
129
129
|
-H "Authorization: Bearer $TOKEN"
|
|
130
130
|
|
|
131
131
|
# Filter by status
|
|
132
|
-
curl "http://localhost:
|
|
132
|
+
curl "http://localhost:8008/v1/executions?status=failed" \
|
|
133
133
|
-H "Authorization: Bearer $TOKEN"
|
|
134
134
|
|
|
135
135
|
# Filter by date range
|
|
136
|
-
curl "http://localhost:
|
|
136
|
+
curl "http://localhost:8008/v1/executions?since=2026-02-01T00:00:00Z&until=2026-02-11T00:00:00Z" \
|
|
137
137
|
-H "Authorization: Bearer $TOKEN"
|
|
138
138
|
|
|
139
139
|
# Executions for a specific trigger
|
|
140
|
-
curl http://localhost:
|
|
140
|
+
curl http://localhost:8008/v1/executions/by-trigger/{triggerId} \
|
|
141
141
|
-H "Authorization: Bearer $TOKEN"
|
|
142
142
|
|
|
143
143
|
# Get execution details
|
|
144
|
-
curl http://localhost:
|
|
144
|
+
curl http://localhost:8008/v1/executions/{id} \
|
|
145
145
|
-H "Authorization: Bearer $TOKEN"
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
### Health Check (no auth)
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
curl http://localhost:
|
|
151
|
+
curl http://localhost:8008/health
|
|
152
152
|
# Returns: { status, service, timestamp, scheduler: { running, tickCount, lastTick } }
|
|
153
153
|
```
|
|
154
154
|
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kortix-deploy
|
|
3
|
+
description: "Deploy any web app, API, or static site to Freestyle.sh with a live preview URL. Supports Next.js, Vite, static sites, Express/Hono APIs, raw code snippets, Git repos, local files, and tar URLs. Auto-detects frameworks, builds, and deploys with instant SSL on *.style.dev subdomains or custom domains. Triggers on: 'deploy this', 'deploy my app', 'make this live', 'publish this', 'host this', 'get me a preview URL', 'deploy to freestyle', 'ship this', 'deploy to production', '1-click deploy', 'preview URL', 'put this online', any request to deploy, host, or publish a web application, site, or API to the internet."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy to Freestyle.sh
|
|
7
|
+
|
|
8
|
+
One-command deploys to live preview URLs via [Freestyle Serverless Deployments](https://docs.freestyle.sh/v2/serverless/deployments). Write a small deploy script, run it, hand back the URL.
|
|
9
|
+
|
|
10
|
+
## About Freestyle Serverless Deployments
|
|
11
|
+
|
|
12
|
+
Freestyle is an API-first serverless platform built for **programmatic deployment at scale** (not dashboard-clicking like Vercel/Netlify). Deployments run **Node.js only** with automatic scaling, wildcard subdomains, and framework detection.
|
|
13
|
+
|
|
14
|
+
**Key capabilities:**
|
|
15
|
+
- **Sub-second deploys** for non-build deployments (no containers, cached deps)
|
|
16
|
+
- **4 source types**: Git repo, inline code, local files (`readFiles`), tar URL
|
|
17
|
+
- **Auto-detects** Next.js, Vite, Expo — TypeScript works out of the box
|
|
18
|
+
- **Free `*.style.dev` subdomains** with instant SSL, custom domains with wildcard certs
|
|
19
|
+
- **WebSocket support** — timeout is per last TCP packet, not HTTP request
|
|
20
|
+
- **Cached modules** — never upload `node_modules`, just your lockfile
|
|
21
|
+
|
|
22
|
+
**When NOT to use Deployments (use Freestyle VMs instead):**
|
|
23
|
+
- Non-Node workloads (Python, Ruby, Go) — VMs are full Linux environments
|
|
24
|
+
- One-shot code execution (no HTTP server needed) — use Serverless Runs
|
|
25
|
+
- Low-level system access (SSH, systemd, filesystem persistence)
|
|
26
|
+
- Browser automation (scraping, testing)
|
|
27
|
+
|
|
28
|
+
## Hard-Won Deployment Lessons
|
|
29
|
+
|
|
30
|
+
These are critical gotchas discovered through real e2e testing:
|
|
31
|
+
|
|
32
|
+
1. **SDK version**: Use `freestyle-sandboxes@latest` (NOT `@beta`). The `@beta` tag (0.1.3) has a different API path (`freestyle.edge.deployments`) that doesn't match current docs. Latest uses `freestyle.serverless.deployments`.
|
|
33
|
+
2. **`readFiles` import**: `import { readFiles } from "freestyle-sandboxes"` — it is exported from the main package. NOT from `freestyle-sandboxes/utils` (that subpath doesn't exist).
|
|
34
|
+
3. **Runtime is Node.js, NOT Deno**: `Deno.serve()` and Hono's `app.fire()` do NOT work. Always use Express `app.listen(3000)` or Hono with `@hono/node-server` and `serve({ fetch: app.fetch, port: 3000 })`.
|
|
35
|
+
4. **Static sites MUST have a Node.js server entrypoint**: Setting `entrypointPath` to an HTML file (e.g., `index.html`) will serve that page but CSS/JS/image sub-assets will NOT load. You MUST bundle an Express static file server.
|
|
36
|
+
5. **Deploy scripts must be `.mjs`**: The SDK uses ESM exports. Write `.mjs` files and run with `node`, not `.ts` with `npx tsx`.
|
|
37
|
+
6. **Cold starts**: First request may 503 for ~10-15 seconds after deploy completes. This is normal. Subsequent requests are instant.
|
|
38
|
+
7. **Port 3000**: All servers must listen on port 3000. This is the port Freestyle routes to.
|
|
39
|
+
8. **`nodeModules` field**: Only needed for `code` deploys. For `files`/`git` deploys, include your lockfile and Freestyle installs deps automatically.
|
|
40
|
+
9. **`envVars` are runtime-only**: NOT available at build time. Use `build.envVars` for build-time env vars.
|
|
41
|
+
10. **Never upload `node_modules`**: `readFiles` auto-excludes it. Freestyle installs from your lockfile.
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
1. **API Key**: `FREESTYLE_API_KEY` must be set. Check with `env | grep FREESTYLE_API_KEY`. If missing, set via kortix-secrets or ask the user for their key from [admin.freestyle.sh](https://admin.freestyle.sh).
|
|
46
|
+
|
|
47
|
+
2. **SDK**: `freestyle-sandboxes` must be installed (already pre-installed in sandbox). If missing:
|
|
48
|
+
```bash
|
|
49
|
+
npm i freestyle-sandboxes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Workflow
|
|
53
|
+
|
|
54
|
+
1. **Detect** project type (see Detection below)
|
|
55
|
+
2. **Pick** the right starter template
|
|
56
|
+
3. **Customize** — fill in project-specific values (repo URL, domain slug, env vars, etc.)
|
|
57
|
+
4. **Write** the deploy script as `.mjs` file (e.g., `/tmp/deploy-freestyle.mjs`)
|
|
58
|
+
5. **Run** it: `FREESTYLE_API_KEY=... node /tmp/deploy-freestyle.mjs`
|
|
59
|
+
6. **Report** the live URL to the user and show it
|
|
60
|
+
|
|
61
|
+
**CRITICAL**: Deploy scripts must be `.mjs` files (ESM). The SDK uses ES module exports. Run with `node`, NOT `npx tsx`.
|
|
62
|
+
|
|
63
|
+
### Domain Naming
|
|
64
|
+
|
|
65
|
+
Generate a descriptive `*.style.dev` subdomain:
|
|
66
|
+
- Use project name or directory name as base
|
|
67
|
+
- Append short random suffix to avoid collisions: `my-app-x7k2.style.dev`
|
|
68
|
+
- Keep it lowercase, alphanumeric + hyphens only
|
|
69
|
+
- Generate with: `` `${slug}-${crypto.randomUUID().slice(0, 4)}.style.dev` ``
|
|
70
|
+
|
|
71
|
+
## Project Detection
|
|
72
|
+
|
|
73
|
+
Check these files to determine the framework. If ambiguous, ask the user.
|
|
74
|
+
|
|
75
|
+
| File present | Framework | Deploy strategy |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `next.config.{js,mjs,ts}` | Next.js | Git + `build: true` or local files via readFiles |
|
|
78
|
+
| `vite.config.{js,ts,mjs}` | Vite | Git + `build: true` or local files via readFiles |
|
|
79
|
+
| Only `.html`/`.css`/`.js` files | Static | Express static server + readFiles |
|
|
80
|
+
| `package.json` with `express`/`hono`/`fastify` dep | Node.js API | Git or local files, no build |
|
|
81
|
+
| `.git` with GitHub remote | Any | Prefer git deploy with repo URL |
|
|
82
|
+
| User provides a code snippet | Code snippet | Inline `code` + `nodeModules` |
|
|
83
|
+
| User provides a URL to `.tar.gz` | Tar | `tarUrl` source |
|
|
84
|
+
|
|
85
|
+
### Next.js Pre-Flight
|
|
86
|
+
|
|
87
|
+
Before deploying Next.js, verify `next.config` has:
|
|
88
|
+
```javascript
|
|
89
|
+
output: "standalone"
|
|
90
|
+
images: { unoptimized: true }
|
|
91
|
+
```
|
|
92
|
+
If missing, add them automatically and inform the user.
|
|
93
|
+
|
|
94
|
+
## Starter Templates
|
|
95
|
+
|
|
96
|
+
Each template is a complete, runnable `.mjs` deploy script. Copy the appropriate one, fill in the `CUSTOMIZE` values, write to `/tmp/deploy-freestyle.mjs`, and run with `FREESTYLE_API_KEY=... node /tmp/deploy-freestyle.mjs`.
|
|
97
|
+
|
|
98
|
+
**Import pattern** (same for all templates):
|
|
99
|
+
```javascript
|
|
100
|
+
import { freestyle, readFiles } from "freestyle-sandboxes"; // readFiles only when deploying local files
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> **NOTE**: `readFiles` is exported from `freestyle-sandboxes` directly. NOT from `freestyle-sandboxes/utils`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### 1. Git Repo Deploy (any framework)
|
|
108
|
+
|
|
109
|
+
The most common path. Works for any project with a Git remote. Freestyle auto-detects Next.js, Vite, Expo.
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
113
|
+
|
|
114
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
115
|
+
repo: "https://github.com/USER/REPO", // CUSTOMIZE: repo URL
|
|
116
|
+
// branch: "main", // CUSTOMIZE: optional
|
|
117
|
+
// rootPath: "./apps/web", // CUSTOMIZE: optional, for monorepos
|
|
118
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE: unique subdomain
|
|
119
|
+
build: true, // set false if no build needed
|
|
120
|
+
// envVars: { KEY: "value" }, // CUSTOMIZE: optional runtime env vars
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
124
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 2. Next.js (from Git)
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
133
|
+
|
|
134
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
135
|
+
repo: "https://github.com/USER/REPO", // CUSTOMIZE
|
|
136
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
137
|
+
build: true, // auto-detects Next.js
|
|
138
|
+
// envVars: { DATABASE_URL: "..." }, // CUSTOMIZE: optional runtime env vars
|
|
139
|
+
// build: { // use this form for build-time env vars
|
|
140
|
+
// command: "npm run build",
|
|
141
|
+
// envVars: { NEXT_PUBLIC_API_URL: "https://api.example.com" },
|
|
142
|
+
// },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
146
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 3. Next.js (from local files)
|
|
152
|
+
|
|
153
|
+
Build locally, copy standalone artifacts, upload with `readFiles`.
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
import { freestyle, readFiles } from "freestyle-sandboxes";
|
|
157
|
+
import { execSync } from "child_process";
|
|
158
|
+
import { cpSync } from "fs";
|
|
159
|
+
|
|
160
|
+
// Build
|
|
161
|
+
execSync("npm run build", { stdio: "inherit", cwd: "PROJECT_DIR" }); // CUSTOMIZE
|
|
162
|
+
|
|
163
|
+
// Prepare standalone artifacts
|
|
164
|
+
cpSync("PROJECT_DIR/public", "PROJECT_DIR/.next/standalone/public", { recursive: true });
|
|
165
|
+
cpSync("PROJECT_DIR/.next/static", "PROJECT_DIR/.next/standalone/.next/static", { recursive: true });
|
|
166
|
+
cpSync("PROJECT_DIR/package-lock.json", "PROJECT_DIR/.next/standalone/package-lock.json"); // CUSTOMIZE: use your lockfile
|
|
167
|
+
|
|
168
|
+
const files = await readFiles("PROJECT_DIR/.next/standalone"); // CUSTOMIZE
|
|
169
|
+
|
|
170
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
171
|
+
files,
|
|
172
|
+
entrypointPath: "server.js",
|
|
173
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
177
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### 4. Vite (from Git)
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
186
|
+
|
|
187
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
188
|
+
repo: "https://github.com/USER/REPO", // CUSTOMIZE
|
|
189
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
190
|
+
build: true, // auto-detects Vite
|
|
191
|
+
// envVars: { VITE_API_URL: "..." }, // CUSTOMIZE: optional
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
195
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### 5. Vite / Static SPA (from local files)
|
|
201
|
+
|
|
202
|
+
Build locally, add an Express static server, deploy the dist. **This pattern works for any pre-built SPA (React, Vue, Svelte, etc.) or static site.**
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
import { freestyle, readFiles } from "freestyle-sandboxes";
|
|
206
|
+
import { execSync } from "child_process";
|
|
207
|
+
import { writeFileSync } from "fs";
|
|
208
|
+
|
|
209
|
+
// Build (skip if already built or pure static)
|
|
210
|
+
execSync("npm run build", { stdio: "inherit", cwd: "PROJECT_DIR" }); // CUSTOMIZE
|
|
211
|
+
|
|
212
|
+
const distDir = "PROJECT_DIR/dist"; // CUSTOMIZE: build output directory
|
|
213
|
+
|
|
214
|
+
// Write Express static server into dist
|
|
215
|
+
writeFileSync(`${distDir}/server.js`, `
|
|
216
|
+
import express from 'express';
|
|
217
|
+
import { fileURLToPath } from 'url';
|
|
218
|
+
import { dirname, join } from 'path';
|
|
219
|
+
|
|
220
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
221
|
+
const app = express();
|
|
222
|
+
|
|
223
|
+
app.use(express.static(__dirname));
|
|
224
|
+
app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html')));
|
|
225
|
+
|
|
226
|
+
app.listen(3000, () => console.log('Server running on port 3000'));
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
writeFileSync(`${distDir}/package.json`, JSON.stringify({
|
|
230
|
+
name: "deploy", type: "module", dependencies: { express: "^4.18.2" }
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const files = await readFiles(distDir);
|
|
234
|
+
|
|
235
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
236
|
+
files,
|
|
237
|
+
entrypointPath: "server.js",
|
|
238
|
+
nodeModules: { express: "^4.18.2" },
|
|
239
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
243
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### 6. Static Site (HTML/CSS/JS files)
|
|
249
|
+
|
|
250
|
+
For plain HTML/CSS/JS with no build step. **Must include an Express server** — Freestyle needs a Node.js entrypoint.
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
import { freestyle, readFiles } from "freestyle-sandboxes";
|
|
254
|
+
import { writeFileSync } from "fs";
|
|
255
|
+
|
|
256
|
+
const siteDir = "PROJECT_DIR"; // CUSTOMIZE: directory with HTML/CSS/JS
|
|
257
|
+
|
|
258
|
+
// Write Express static server
|
|
259
|
+
writeFileSync(`${siteDir}/server.js`, `
|
|
260
|
+
import express from 'express';
|
|
261
|
+
import { fileURLToPath } from 'url';
|
|
262
|
+
import { dirname, join } from 'path';
|
|
263
|
+
|
|
264
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
265
|
+
const app = express();
|
|
266
|
+
|
|
267
|
+
app.use(express.static(__dirname));
|
|
268
|
+
app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html')));
|
|
269
|
+
|
|
270
|
+
app.listen(3000, () => console.log('Static server on port 3000'));
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
const files = await readFiles(siteDir);
|
|
274
|
+
|
|
275
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
276
|
+
files,
|
|
277
|
+
entrypointPath: "server.js",
|
|
278
|
+
nodeModules: { express: "^4.18.2" },
|
|
279
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
283
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### 7. Code Snippet Deploy (Express)
|
|
289
|
+
|
|
290
|
+
For quick API servers or demos. **Use Express with `app.listen(3000)`** — this is the proven pattern.
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
294
|
+
|
|
295
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
296
|
+
code: `
|
|
297
|
+
import express from 'express';
|
|
298
|
+
const app = express();
|
|
299
|
+
|
|
300
|
+
app.get('/', (req, res) => {
|
|
301
|
+
res.json({ status: 'ok', time: new Date().toISOString() });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.listen(3000, () => console.log('Running on port 3000'));
|
|
305
|
+
`,
|
|
306
|
+
nodeModules: {
|
|
307
|
+
express: "^4.18.2", // CUSTOMIZE: dependencies
|
|
308
|
+
},
|
|
309
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
310
|
+
// envVars: { KEY: "value" }, // CUSTOMIZE: optional
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
314
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Hono variant** (also works — must use `@hono/node-server`):
|
|
318
|
+
```javascript
|
|
319
|
+
code: `
|
|
320
|
+
import { Hono } from "hono";
|
|
321
|
+
import { serve } from "@hono/node-server";
|
|
322
|
+
const app = new Hono();
|
|
323
|
+
app.get("/", (c) => c.json({ status: "ok" }));
|
|
324
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
325
|
+
`,
|
|
326
|
+
nodeModules: { hono: "4.11.1", "@hono/node-server": "^1.13.8" },
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> **WARNING**: Do NOT use `app.fire()` or `Deno.serve()` — these do not work in the Freestyle runtime. Always use `app.listen(3000)` (Express) or `serve({ fetch: app.fetch, port: 3000 })` (Hono).
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### 8. Tar URL Deploy
|
|
334
|
+
|
|
335
|
+
For deploying from a remote archive (S3, GCS signed URL, etc).
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
339
|
+
|
|
340
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({
|
|
341
|
+
tarUrl: "https://s3.example.com/signed-url/app.tar.gz", // CUSTOMIZE
|
|
342
|
+
domains: ["SLUG.style.dev"], // CUSTOMIZE
|
|
343
|
+
build: true, // CUSTOMIZE: set false if pre-built
|
|
344
|
+
// entrypointPath: "server.js", // CUSTOMIZE: if needed
|
|
345
|
+
// envVars: { KEY: "value" }, // CUSTOMIZE: optional
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
|
|
349
|
+
console.log("Deployment ID:", deployment.deploymentId);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Custom Domains
|
|
353
|
+
|
|
354
|
+
If the user wants to deploy to their own domain instead of `*.style.dev`:
|
|
355
|
+
|
|
356
|
+
### 1. Verify domain ownership
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
const { record, instructions } = await freestyle.domains.verifications.create({
|
|
360
|
+
domain: "example.com",
|
|
361
|
+
});
|
|
362
|
+
// Tell user: Add TXT record _freestyle_custom_hostname.example.com → record.value
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### 2. Complete verification (after user adds DNS record)
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
await freestyle.domains.verifications.complete({ domain: "example.com" });
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 3. Configure DNS
|
|
372
|
+
|
|
373
|
+
Tell the user to add an A record pointing to `35.235.84.134`:
|
|
374
|
+
- **APEX** (`example.com`): `A @ 35.235.84.134`
|
|
375
|
+
- **Subdomain** (`app.example.com`): `A app 35.235.84.134`
|
|
376
|
+
- **Wildcard** (`*.example.com`): `A * 35.235.84.134`
|
|
377
|
+
|
|
378
|
+
### 4. Deploy
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
domains: ["example.com"] // use the verified domain in the deploy call
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## API Reference (Quick)
|
|
385
|
+
|
|
386
|
+
Full details in `references/freestyle-api.md`. Key options for `freestyle.serverless.deployments.create()`:
|
|
387
|
+
|
|
388
|
+
**Sources** (exactly one required):
|
|
389
|
+
- `repo: "https://github.com/user/repo"` + optional `branch`, `rootPath`
|
|
390
|
+
- `code: "..."` + `nodeModules: { pkg: "version" }`
|
|
391
|
+
- `files` (from `readFiles(dir)`) + `entrypointPath: "server.js"`
|
|
392
|
+
- `tarUrl: "https://..."`
|
|
393
|
+
|
|
394
|
+
**Options:**
|
|
395
|
+
- `domains: ["slug.style.dev"]` — required, free `*.style.dev` or verified custom domain
|
|
396
|
+
- `build: true` or `build: { command, outDir, envVars }` — triggers framework build
|
|
397
|
+
- `entrypointPath: "server.js"` — main file (auto-detected for Next.js/Vite)
|
|
398
|
+
- `envVars: { KEY: "value" }` — runtime env vars (NOT build-time)
|
|
399
|
+
- `nodeModules: { express: "^4.18.2" }` — only for `code` deploys
|
|
400
|
+
- `timeoutMs: 60000` — idle timeout before scale-down (per last TCP packet)
|
|
401
|
+
- `networkPermissions: [{ action, domain, behavior }]` — outbound network ACL
|
|
402
|
+
- `headers: [{ source, headers: [{ key, value }] }]` — custom response headers
|
|
403
|
+
- `redirects: [{ source, destination, permanent }]` — URL redirects
|
|
404
|
+
- `waitForRollout: true` — wait until fully serving traffic
|
|
405
|
+
|
|
406
|
+
**Return value:**
|
|
407
|
+
```javascript
|
|
408
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({...});
|
|
409
|
+
// deployment.deploymentId — unique ID
|
|
410
|
+
// domains — string[] of live URLs
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Post-Deploy
|
|
414
|
+
|
|
415
|
+
After a successful deployment:
|
|
416
|
+
|
|
417
|
+
1. **Show the live URL** prominently — `https://SLUG.style.dev`
|
|
418
|
+
2. **Show deployment ID** — for reference and debugging
|
|
419
|
+
3. **Open in browser** if the kortix-browser skill is available:
|
|
420
|
+
```bash
|
|
421
|
+
agent-browser --session preview-deploy open https://SLUG.style.dev
|
|
422
|
+
```
|
|
423
|
+
4. **Clean up** — remove `/tmp/deploy-freestyle.mjs`
|
|
424
|
+
|
|
425
|
+
## Troubleshooting
|
|
426
|
+
|
|
427
|
+
| Problem | Fix |
|
|
428
|
+
|---|---|
|
|
429
|
+
| `FREESTYLE_API_KEY` not set | Set via kortix-secrets or `export FREESTYLE_API_KEY=...` |
|
|
430
|
+
| `Cannot find package` | Run deploy script from project root where `freestyle-sandboxes` is installed, or use absolute path to node_modules |
|
|
431
|
+
| Build fails for Next.js | Ensure `output: "standalone"` and `images: { unoptimized: true }` in next.config |
|
|
432
|
+
| Module not found on Freestyle | Include lockfile in source. Freestyle installs deps from it. Never upload `node_modules`. |
|
|
433
|
+
| 404 on SPA routes / static assets not loading | You MUST use an Express static server entrypoint. Setting `entrypointPath` to an HTML file does NOT work for sub-assets. |
|
|
434
|
+
| `app.fire()` / `Deno.serve()` fails | Freestyle runs Node.js, not Deno. Use `app.listen(3000)` or `@hono/node-server` `serve()`. |
|
|
435
|
+
| 503 on first request | Cold start — wait 10-15 seconds after deploy for the instance to warm up. |
|
|
436
|
+
| Domain not working | Check DNS: `dig yourdomain.com` should show `35.235.84.134` |
|
|
437
|
+
| Deploy takes too long | Use `await: false` to return immediately, then poll with `freestyle.serverless.deployments.get()` |
|
|
438
|
+
| Subdomain taken | Pick a different `*.style.dev` slug — add more random chars |
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Freestyle Serverless Deployments - Full API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for `freestyle.serverless.deployments.create()`.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { freestyle, readFiles } from "freestyle-sandboxes"; // readFiles for local file deploys
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires `FREESTYLE_API_KEY` environment variable.
|
|
12
|
+
|
|
13
|
+
## Source Types (exactly one required)
|
|
14
|
+
|
|
15
|
+
### Git Repository
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
{
|
|
19
|
+
repo: "https://github.com/user/repo", // public URL, authenticated URL, or freestyle repo ID
|
|
20
|
+
branch: "main", // optional, defaults to default branch
|
|
21
|
+
rootPath: "./apps/web", // optional, for monorepos
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Private repos: use `https://user:token@github.com/user/repo.git` or a Freestyle Git repo ID.
|
|
26
|
+
|
|
27
|
+
### Inline Code
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
{
|
|
31
|
+
code: `
|
|
32
|
+
import express from 'express';
|
|
33
|
+
const app = express();
|
|
34
|
+
app.get('/', (req, res) => res.send('Hello'));
|
|
35
|
+
app.listen(3000);
|
|
36
|
+
`,
|
|
37
|
+
nodeModules: { // required with code deploys
|
|
38
|
+
express: "^4.18.2",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Local Files (readFiles)
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import { readFiles } from "freestyle-sandboxes"; // NOT from /utils
|
|
47
|
+
|
|
48
|
+
const files = await readFiles("./dist"); // reads dir, excludes node_modules, base64-encodes
|
|
49
|
+
{
|
|
50
|
+
files,
|
|
51
|
+
entrypointPath: "server.js", // required for file deploys - MUST be a Node.js server, not an HTML file
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`readFiles` automatically excludes `node_modules/` and handles binary files. The `entrypointPath` MUST point to a Node.js server file (e.g., Express/Hono) — pointing it at an HTML file will not serve sub-assets.
|
|
56
|
+
|
|
57
|
+
### Tar URL
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
{
|
|
61
|
+
tarUrl: "https://s3.example.com/signed-url/app.tar.gz",
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Useful when source is hosted externally (S3, GCS, etc).
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
### domains (required)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
domains: ["my-app.style.dev"] // free *.style.dev subdomain
|
|
73
|
+
domains: ["app.style.dev", "app.yourdomain.com"] // multiple domains
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Any unclaimed `*.style.dev` subdomain works instantly with SSL. Custom domains need verification first.
|
|
77
|
+
|
|
78
|
+
### build
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
build: true // auto-detect framework, run build
|
|
82
|
+
build: {
|
|
83
|
+
command: "npm run build", // custom build command
|
|
84
|
+
outDir: "dist", // build output directory
|
|
85
|
+
envVars: { // build-time env vars (NOT available at runtime)
|
|
86
|
+
NEXT_PUBLIC_API_URL: "https://api.example.com",
|
|
87
|
+
NODE_ENV: "production",
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Default: `false` (deploy files as-is). Set `true` for Next.js, Vite, and other frameworks requiring a build step. Freestyle auto-detects Next.js, Vite, and Expo.
|
|
93
|
+
|
|
94
|
+
### entrypoint
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
entrypoint: "server.js" // main file of your application
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Auto-detected for Next.js, Vite, Expo. Specify manually for custom setups.
|
|
101
|
+
|
|
102
|
+
### envVars
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
envVars: {
|
|
106
|
+
API_KEY: "secret-value", // available at RUNTIME only
|
|
107
|
+
DATABASE_URL: "postgres://...",
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
NOT available at build time. For build-time vars, use `build.envVars`. Env vars are tied to deployments; to change them, create a new deployment.
|
|
112
|
+
|
|
113
|
+
### nodeModules
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
nodeModules: {
|
|
117
|
+
express: "^4.18.2",
|
|
118
|
+
cors: "^2.8.5",
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Only needed for `code` deploys. For git/file deploys, include your lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, or `bun.lock`) and Freestyle installs from it.
|
|
123
|
+
|
|
124
|
+
### timeoutMs
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
timeoutMs: 60000 // milliseconds, idle timeout before scale-down
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Timeout is from last TCP packet (not HTTP request), so WebSockets stay alive as long as you ping faster than the timeout.
|
|
131
|
+
|
|
132
|
+
### networkPermissions
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
networkPermissions: [
|
|
136
|
+
{ action: "allow", domain: "api.stripe.com", behavior: "exact" },
|
|
137
|
+
{ action: "allow", domain: ".*\\.amazonaws\\.com", behavior: "regex" },
|
|
138
|
+
]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### await
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
await: false // return immediately with deploymentId for polling
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Default: `true` (waits for deployment to build and propagate). Set `false` to return immediately.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Polling pattern
|
|
151
|
+
const { deploymentId } = await freestyle.serverless.deployments.create({
|
|
152
|
+
...,
|
|
153
|
+
await: false,
|
|
154
|
+
});
|
|
155
|
+
const status = await freestyle.serverless.deployments.get({ deploymentId });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### waitForRollout
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
waitForRollout: true // wait until deployment is fully serving traffic
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### staticOnly
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
staticOnly: true,
|
|
168
|
+
publicDir: "dist", // directory with static files
|
|
169
|
+
cleanUrls: true, // /about.html becomes /about
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Serves files directly without a server entrypoint. For pure static sites.
|
|
173
|
+
|
|
174
|
+
### headers
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
headers: [
|
|
178
|
+
{
|
|
179
|
+
source: "^/assets/.*$",
|
|
180
|
+
headers: [{ key: "Cache-Control", value: "max-age=31536000, immutable" }],
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### redirects
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
redirects: [
|
|
189
|
+
{ source: "^/old-page$", destination: "/new-page", permanent: true },
|
|
190
|
+
]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Return Value
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const { deployment, domains } = await freestyle.serverless.deployments.create({...});
|
|
197
|
+
|
|
198
|
+
deployment.deploymentId // string - unique deployment ID
|
|
199
|
+
domains // string[] - live URLs
|
|
200
|
+
|
|
201
|
+
// With await: false
|
|
202
|
+
const { deploymentId } = await freestyle.serverless.deployments.create({..., await: false});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Custom Domains
|
|
206
|
+
|
|
207
|
+
### Step 1: Verify ownership
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const { record, instructions } = await freestyle.domains.verifications.create({
|
|
211
|
+
domain: "example.com",
|
|
212
|
+
});
|
|
213
|
+
// Add TXT record: _freestyle_custom_hostname.example.com → <verification-code>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Step 2: Complete verification
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
await freestyle.domains.verifications.complete({ domain: "example.com" });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Step 3: Configure DNS
|
|
223
|
+
|
|
224
|
+
Point domain to Freestyle:
|
|
225
|
+
- **APEX** (`example.com`): A record → `35.235.84.134`
|
|
226
|
+
- **Subdomain** (`app.example.com`): A record for `app` → `35.235.84.134`
|
|
227
|
+
- **Wildcard** (`*.example.com`): A record for `*` → `35.235.84.134`
|
|
228
|
+
|
|
229
|
+
### Step 4: Deploy
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
await freestyle.serverless.deployments.create({
|
|
233
|
+
...,
|
|
234
|
+
domains: ["example.com"],
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Framework Notes
|
|
239
|
+
|
|
240
|
+
### Next.js
|
|
241
|
+
|
|
242
|
+
Requires in `next.config.mjs`:
|
|
243
|
+
```javascript
|
|
244
|
+
const nextConfig = {
|
|
245
|
+
output: "standalone", // required
|
|
246
|
+
images: { unoptimized: true }, // required (no Sharp on Freestyle)
|
|
247
|
+
};
|
|
248
|
+
export default nextConfig;
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
For local file deploys, after `npm run build`:
|
|
252
|
+
```bash
|
|
253
|
+
cp -r public .next/standalone/public
|
|
254
|
+
cp -r .next/static .next/standalone/.next/static
|
|
255
|
+
cp package-lock.json .next/standalone/package-lock.json
|
|
256
|
+
```
|
|
257
|
+
Then `readFiles(".next/standalone")` with `entrypointPath: "server.js"`.
|
|
258
|
+
|
|
259
|
+
### Vite
|
|
260
|
+
|
|
261
|
+
Auto-detected when deploying from git with `build: true`. For local file deploys, use an Express static server:
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
import express from 'express';
|
|
265
|
+
import { fileURLToPath } from 'url';
|
|
266
|
+
import { dirname, join } from 'path';
|
|
267
|
+
|
|
268
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
269
|
+
const app = express();
|
|
270
|
+
app.use(express.static(__dirname));
|
|
271
|
+
app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html'))); // SPA fallback
|
|
272
|
+
app.listen(3000);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
> **WARNING**: Do NOT use `Deno.serve()` or Hono's `app.fire()`. Freestyle runs Node.js. Use `app.listen(3000)` (Express) or `serve({ fetch: app.fetch, port: 3000 })` from `@hono/node-server`.
|
|
276
|
+
|
|
277
|
+
### Static Sites
|
|
278
|
+
|
|
279
|
+
Static sites MUST have an Express/Hono Node.js server entrypoint. Use the same Express pattern as Vite above.
|