Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit c82ed27

Browse files
committed
feat: freestyle example
1 parent 16c0824 commit c82ed27

File tree

16 files changed

+654
-0
lines changed

16 files changed

+654
-0
lines changed

examples/freestyle/.env.sample

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FREESTYLE_API_KEY=""
2+
FREESTYLE_DOMAIN="example.style.dev"
3+
RIVET_TOKEN=""
4+
5+
# To enable serverless
6+
RIVET_ENDPOINT=""
7+
RIVET_RUNNER_KIND="serverless"
8+
9+
# For accessing actor (not relevant if using self-hosted Rivet Engine)
10+
RIVET_PUBLISHABLE_TOKEN="" # default: ""
11+
12+
# Optional variables with defaults:
13+
RIVET_NAMESPACE="default" # default: "default"
14+
RIVET_RUNNER_NAME="freestyle-runner" # default: "freestyle-runner"

examples/freestyle/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.actorcore
2+
node_modules

examples/freestyle/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Freestyle Deployment for RivetKit
2+
3+
Example project demonstrating serverless deployment of RivetKit actors to [Freestyle](https://freestyle.sh) with [RivetKit](https://rivetkit.org).
4+
5+
[Learn More →](https://github.com/rivet-dev/rivetkit)
6+
7+
[Discord](https://rivet.dev/discord)[Documentation](https://rivetkit.org)[Issues](https://github.com/rivet-dev/rivetkit/issues)
8+
9+
10+
## What is this?
11+
12+
Freestyle is unique from other providers since it is built to deploy untrusted AI-generated or user-generated code. This enables your application to deploy vibe-coded or user-provided backends on Rivet and Freestyle. This example showcases a real-time stateful chat app that can be deployed to FreeStyle's [Web Deployment](https://docs.freestyle.sh/web/overview) platform.
13+
14+
## Getting Started
15+
16+
### Prerequisites
17+
18+
- Node.js 18+
19+
- Deno (for development)
20+
21+
**Note**: Deno is required since Freestyle uses Deno for their Web Deployments under the hood
22+
23+
### Installation
24+
25+
```sh
26+
git clone https://github.com/rivet-dev/rivetkit
27+
cd rivetkit/examples/freestyle
28+
pnpm install
29+
```
30+
31+
### Development
32+
33+
```sh
34+
pnpm run dev
35+
```
36+
37+
Open your browser to `http://localhost:5173` to see the application.
38+
39+
```sh
40+
RIVET_RUNNER_KIND=serverless VITE_RIVET_ENDPOINT="$RIVET_ENDPOINT" pnpm run dev
41+
```
42+
43+
### Deploy to Freestyle
44+
45+
```sh
46+
# Set env vars
47+
export FREESTYLE_DOMAIN="my-domain.style.dev" # Set this to any unique *.style.dev domain
48+
export FREESTYLE_API_KEY="XXXX" # See https://admin.freestyle.sh/dashboard/api-tokens
49+
export RIVET_ENDPOINT="http://api.rivet.gg"
50+
export RIVET_NAMESPACE="XXXX" # Creates new namespace if does not exist
51+
export RIVET_TOKEN="XXXX" # Rivet Service token
52+
export RIVET_PUBLISHABLE_TOKEN="XXXX" # For connecting to Rivet Actors
53+
54+
pnpm run freestyle:deploy
55+
```
56+
57+
Open your browser to your Freestyle domain to see your application connect to Rivet deployed on Freestyle.
58+
59+
If self-hosting Rivet:
60+
1. **Important**: `RIVET_ENDPOINT` must be public to the internet.
61+
2. `RIVET_PUBLISHABLE_TOKEN` can be kept empty.
62+
63+
## License
64+
65+
Apache 2.0

examples/freestyle/deno.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"imports": {
3+
"os": "node:os",
4+
"path": "node:path",
5+
"fs": "node:fs",
6+
"fs/promises": "node:fs/promises",
7+
"crypto": "node:crypto",
8+
"rivetkit": "npm:rivetkit",
9+
"hono": "npm:hono"
10+
}
11+
}

examples/freestyle/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "example-freestyle",
3+
"version": "2.0.7-rc.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "concurrently \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
8+
"dev:backend": "deno run --watch --allow-all src/backend/server.ts",
9+
"dev:frontend": "vite",
10+
"freestyle:deploy": "tsx scripts/freestyle-deploy.ts",
11+
"check-types": "tsc --noEmit"
12+
},
13+
"devDependencies": {
14+
"@rivetkit/engine-api-full": "^25.7.2",
15+
"@rivetkit/react": "workspace:*",
16+
"@types/node": "^22.13.9",
17+
"@types/prompts": "^2",
18+
"@types/react": "^18.2.0",
19+
"@types/react-dom": "^18.2.0",
20+
"@vitejs/plugin-react": "^4.2.0",
21+
"concurrently": "^8.2.2",
22+
"dotenv": "^17.2.2",
23+
"freestyle-sandboxes": "^0.0.95",
24+
"prompts": "^2.4.2",
25+
"react": "^18.2.0",
26+
"react-dom": "^18.2.0",
27+
"tsup": "^8.5.0",
28+
"tsx": "^3.12.7",
29+
"typescript": "^5.5.2",
30+
"vite": "^5.0.0",
31+
"vitest": "^3.1.1"
32+
},
33+
"dependencies": {
34+
"hono": "4.9.8",
35+
"rivetkit": "workspace:*"
36+
},
37+
"stableVersion": "0.8.0"
38+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { type Rivet, RivetClient } from "@rivetkit/engine-api-full";
2+
import { execSync } from "child_process";
3+
import dotenv from "dotenv";
4+
import { FreestyleSandboxes } from "freestyle-sandboxes";
5+
import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils";
6+
import { readFileSync } from "fs";
7+
8+
dotenv.config({ path: new URL("../.env", import.meta.url).pathname });
9+
10+
const FREESTYLE_DOMAIN = getEnv("FREESTYLE_DOMAIN");
11+
const FREESTYLE_API_KEY = getEnv("FREESTYLE_API_KEY");
12+
const RIVET_ENDPOINT = getEnv("RIVET_ENDPOINT");
13+
const RIVET_TOKEN = getEnv("RIVET_TOKEN");
14+
const RIVET_NAMESPACE_NAME = getEnv("RIVET_NAMESPACE");
15+
const RIVET_RUNNER_NAME = "freestyle";
16+
17+
function getEnv(key: string): string {
18+
const value = process.env[key];
19+
if (typeof value === "undefined") {
20+
throw new Error(`Missing env var: ${key}`);
21+
}
22+
return value;
23+
}
24+
25+
const rivet = new RivetClient({
26+
environment: RIVET_ENDPOINT,
27+
token: RIVET_TOKEN,
28+
});
29+
30+
const freestyle = new FreestyleSandboxes({
31+
apiKey: FREESTYLE_API_KEY,
32+
});
33+
34+
async function main() {
35+
const namespace = await getOrCreateNamespace({
36+
displayName: RIVET_NAMESPACE_NAME,
37+
name: RIVET_NAMESPACE_NAME,
38+
});
39+
console.log("Got namespace " + namespace.name);
40+
41+
runBuildSteps();
42+
43+
await deployToFreestyle();
44+
console.log("Deployed to freestyle");
45+
46+
await updateRunnerConfig(namespace);
47+
console.log("Updated runner config");
48+
49+
console.log("🎉 Deployment complete! 🎉");
50+
console.log(
51+
"Visit https://" +
52+
FREESTYLE_DOMAIN +
53+
"/ to see your frontend, which is connected to Rivet",
54+
);
55+
}
56+
57+
function runBuildSteps() {
58+
console.log("Running build steps...");
59+
60+
console.log("- Running vite build");
61+
execSync("vite build", {
62+
stdio: "inherit",
63+
env: {
64+
...process.env,
65+
VITE_RIVET_ENDPOINT: RIVET_ENDPOINT,
66+
VITE_RIVET_NAMESPACE: RIVET_NAMESPACE_NAME,
67+
VITE_RIVET_RUNNER_NAME: RIVET_RUNNER_NAME,
68+
},
69+
});
70+
71+
console.log("- Running tsup");
72+
execSync("tsup", {
73+
stdio: "inherit",
74+
});
75+
76+
console.log("Build complete!");
77+
}
78+
79+
async function getOrCreateNamespace({
80+
name,
81+
displayName,
82+
}: {
83+
name: string;
84+
displayName: string;
85+
}): Promise<Rivet.Namespace> {
86+
console.log("- Checking for existing " + name + " namespace");
87+
const { namespaces } = await rivet.namespaces.list({
88+
limit: 32,
89+
});
90+
const existing = namespaces.find((ns) => ns.name === name);
91+
if (existing) {
92+
console.log("- Found existing namespace " + name);
93+
return existing;
94+
}
95+
console.log("- Creating namespace " + name);
96+
const { namespace } = await rivet.namespaces.create({
97+
displayName,
98+
name,
99+
});
100+
return namespace;
101+
}
102+
103+
async function updateRunnerConfig(namespace: Rivet.Namespace) {
104+
console.log("- Updating runner config for " + RIVET_RUNNER_NAME);
105+
await rivet.runnerConfigs.upsert(RIVET_RUNNER_NAME, {
106+
serverless: {
107+
url: "https://" + FREESTYLE_DOMAIN + "/api/start",
108+
headers: {},
109+
runnersMargin: 1,
110+
minRunners: 1,
111+
maxRunners: 1,
112+
slotsPerRunner: 1,
113+
requestLifespan: 100,
114+
},
115+
namespace: namespace.name,
116+
});
117+
}
118+
119+
async function deployToFreestyle() {
120+
console.log("- Deploying to freestyle at https://" + FREESTYLE_DOMAIN);
121+
122+
const buildDir = prepareDirForDeploymentSync(
123+
new URL("../dist", import.meta.url).pathname,
124+
);
125+
if (buildDir.kind === "files") {
126+
buildDir.files["deno.json"] = {
127+
// Fix imports for Deno
128+
content: readFileSync(
129+
new URL("../deno.json", import.meta.url).pathname,
130+
"utf-8",
131+
),
132+
encoding: "utf-8",
133+
};
134+
} else {
135+
throw new Error("Expected buildDir to be files");
136+
}
137+
const res = await freestyle.deployWeb(buildDir, {
138+
envVars: {
139+
LOG_LEVEL: "debug",
140+
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`,
141+
RIVET_ENDPOINT,
142+
RIVET_RUNNER_KIND: "serverless",
143+
},
144+
timeout: 120,
145+
entrypoint: "server.cjs",
146+
domains: [FREESTYLE_DOMAIN],
147+
build: false,
148+
});
149+
150+
console.log("Deployment id=" + res.deploymentId);
151+
}
152+
153+
main();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { actor, setup } from "rivetkit";
2+
3+
export type Message = { sender: string; text: string; timestamp: number };
4+
5+
export const chatRoom = actor({
6+
// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
7+
state: {
8+
messages: [] as Message[],
9+
},
10+
11+
actions: {
12+
// Callable functions from clients: https://rivet.dev/docs/actors/actions
13+
sendMessage: (c, sender: string, text: string) => {
14+
const message = { sender, text, timestamp: Date.now() };
15+
// State changes are automatically persisted
16+
c.state.messages.push(message);
17+
// Send events to all connected clients: https://rivet.dev/docs/actors/events
18+
c.broadcast("newMessage", message);
19+
return message;
20+
},
21+
22+
getHistory: (c) => c.state.messages,
23+
},
24+
});
25+
26+
// Register actors for use: https://rivet.dev/docs/setup
27+
export const registry = setup({
28+
use: { chatRoom },
29+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Hono } from "hono";
2+
import { serveStatic, upgradeWebSocket } from "hono/deno";
3+
import { registry } from "./registry";
4+
5+
const serverOutput = registry.start({
6+
inspector: {
7+
enabled: true,
8+
},
9+
disableDefaultServer: true,
10+
basePath: "/api",
11+
getUpgradeWebSocket: () => upgradeWebSocket,
12+
overrideServerAddress: `${process.env.FREESTYLE_ENDPOINT ?? "http://localhost:8080"}/api`,
13+
cors: {
14+
origin: process.env.FREESTYLE_ENDPOINT ?? "http://localhost:5173",
15+
credentials: true,
16+
},
17+
});
18+
19+
const app = new Hono();
20+
app.use("/api/*", async (c) => {
21+
return await serverOutput.fetch(c.req.raw);
22+
});
23+
app.use("*", serveStatic({ root: "./public" }));
24+
25+
// Under the hood, Freestyle uses Deno
26+
// for their Web Deploy instances
27+
// @ts-ignore
28+
Deno.serve({ port: 8080 }, app.fetch);

0 commit comments

Comments
 (0)