-
Notifications
You must be signed in to change notification settings - Fork 41
feat: freestyle example #1317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: freestyle example #1317
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FREESTYLE_API_KEY="" | ||
FREESTYLE_DOMAIN="example.style.dev" | ||
RIVET_TOKEN="" | ||
|
||
# To enable serverless | ||
RIVET_ENDPOINT="" | ||
RIVET_RUNNER_KIND="serverless" | ||
|
||
# For accessing actor (not relevant if using self-hosted Rivet Engine) | ||
RIVET_PUBLISHABLE_TOKEN="" # default: "" | ||
|
||
# Optional variables with defaults: | ||
RIVET_NAMESPACE="default" # default: "default" | ||
RIVET_RUNNER_NAME="freestyle-runner" # default: "freestyle-runner" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.actorcore | ||
node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Freestyle Deployment for RivetKit | ||
|
||
Example project demonstrating serverless deployment of RivetKit actors to [Freestyle](https://freestyle.sh) with [RivetKit](https://rivetkit.org). | ||
|
||
[Learn More →](https://github.com/rivet-dev/rivetkit) | ||
|
||
[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues) | ||
|
||
|
||
## What is this? | ||
|
||
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. | ||
|
||
## Getting Started | ||
|
||
### Prerequisites | ||
|
||
- Node.js 18+ | ||
- Deno (for development) | ||
|
||
**Note**: Deno is required since Freestyle uses Deno for their Web Deployments under the hood | ||
|
||
### Installation | ||
|
||
```sh | ||
git clone https://github.com/rivet-dev/rivetkit | ||
cd rivetkit/examples/freestyle | ||
pnpm install | ||
``` | ||
|
||
### Development | ||
|
||
```sh | ||
pnpm run dev | ||
``` | ||
|
||
Open your browser to `http://localhost:5173` to see the application. | ||
|
||
```sh | ||
RIVET_RUNNER_KIND=serverless VITE_RIVET_ENDPOINT="$RIVET_ENDPOINT" pnpm run dev | ||
``` | ||
|
||
### Deploy to Freestyle | ||
|
||
```sh | ||
# Set env vars | ||
export FREESTYLE_DOMAIN="my-domain.style.dev" # Set this to any unique *.style.dev domain | ||
export FREESTYLE_API_KEY="XXXX" # See https://admin.freestyle.sh/dashboard/api-tokens | ||
export RIVET_ENDPOINT="http://api.rivet.gg" | ||
export RIVET_NAMESPACE="XXXX" # Creates new namespace if does not exist | ||
export RIVET_TOKEN="XXXX" # Rivet Service token | ||
export RIVET_PUBLISHABLE_TOKEN="XXXX" # For connecting to Rivet Actors | ||
|
||
pnpm run freestyle:deploy | ||
``` | ||
|
||
Open your browser to your Freestyle domain to see your application connect to Rivet deployed on Freestyle. | ||
|
||
If self-hosting Rivet: | ||
1. **Important**: `RIVET_ENDPOINT` must be public to the internet. | ||
2. `RIVET_PUBLISHABLE_TOKEN` can be kept empty. | ||
|
||
## License | ||
|
||
Apache 2.0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"imports": { | ||
"os": "node:os", | ||
"path": "node:path", | ||
"fs": "node:fs", | ||
"fs/promises": "node:fs/promises", | ||
"crypto": "node:crypto", | ||
"rivetkit": "npm:rivetkit", | ||
"hono": "npm:hono" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "example-freestyle", | ||
"version": "2.0.7-rc.1", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"dev": "concurrently \"pnpm run dev:backend\" \"pnpm run dev:frontend\"", | ||
"dev:backend": "deno run --watch --allow-all src/backend/server.ts", | ||
"dev:frontend": "vite", | ||
"freestyle:deploy": "tsx scripts/freestyle-deploy.ts", | ||
"check-types": "tsc --noEmit" | ||
}, | ||
"devDependencies": { | ||
"@rivetkit/engine-api-full": "^25.7.2", | ||
"@rivetkit/react": "workspace:*", | ||
"@types/node": "^22.13.9", | ||
"@types/prompts": "^2", | ||
"@types/react": "^18.2.0", | ||
"@types/react-dom": "^18.2.0", | ||
"@vitejs/plugin-react": "^4.2.0", | ||
"concurrently": "^8.2.2", | ||
"dotenv": "^17.2.2", | ||
"freestyle-sandboxes": "^0.0.95", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dependency Spotted by Diamond This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here. |
||
"prompts": "^2.4.2", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"tsup": "^8.5.0", | ||
"tsx": "^3.12.7", | ||
"typescript": "^5.5.2", | ||
"vite": "^5.0.0", | ||
"vitest": "^3.1.1" | ||
}, | ||
"dependencies": { | ||
"hono": "4.9.8", | ||
"rivetkit": "workspace:*" | ||
}, | ||
"stableVersion": "0.8.0" | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,153 @@ | ||||||||||||||||||||||||||||||||
import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; | ||||||||||||||||||||||||||||||||
import { execSync } from "child_process"; | ||||||||||||||||||||||||||||||||
import dotenv from "dotenv"; | ||||||||||||||||||||||||||||||||
import { FreestyleSandboxes } from "freestyle-sandboxes"; | ||||||||||||||||||||||||||||||||
import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; | ||||||||||||||||||||||||||||||||
import { readFileSync } from "fs"; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
dotenv.config({ path: new URL("../.env", import.meta.url).pathname }); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const FREESTYLE_DOMAIN = getEnv("FREESTYLE_DOMAIN"); | ||||||||||||||||||||||||||||||||
const FREESTYLE_API_KEY = getEnv("FREESTYLE_API_KEY"); | ||||||||||||||||||||||||||||||||
const RIVET_ENDPOINT = getEnv("RIVET_ENDPOINT"); | ||||||||||||||||||||||||||||||||
const RIVET_TOKEN = getEnv("RIVET_TOKEN"); | ||||||||||||||||||||||||||||||||
const RIVET_NAMESPACE_NAME = getEnv("RIVET_NAMESPACE"); | ||||||||||||||||||||||||||||||||
const RIVET_RUNNER_NAME = "freestyle"; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The runner name
Suggested change
Spotted by Diamond This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here. |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
function getEnv(key: string): string { | ||||||||||||||||||||||||||||||||
const value = process.env[key]; | ||||||||||||||||||||||||||||||||
if (typeof value === "undefined") { | ||||||||||||||||||||||||||||||||
throw new Error(`Missing env var: ${key}`); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
return value; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const rivet = new RivetClient({ | ||||||||||||||||||||||||||||||||
environment: RIVET_ENDPOINT, | ||||||||||||||||||||||||||||||||
token: RIVET_TOKEN, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const freestyle = new FreestyleSandboxes({ | ||||||||||||||||||||||||||||||||
apiKey: FREESTYLE_API_KEY, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async function main() { | ||||||||||||||||||||||||||||||||
const namespace = await getOrCreateNamespace({ | ||||||||||||||||||||||||||||||||
displayName: RIVET_NAMESPACE_NAME, | ||||||||||||||||||||||||||||||||
name: RIVET_NAMESPACE_NAME, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
console.log("Got namespace " + namespace.name); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
runBuildSteps(); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
await deployToFreestyle(); | ||||||||||||||||||||||||||||||||
console.log("Deployed to freestyle"); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
await updateRunnerConfig(namespace); | ||||||||||||||||||||||||||||||||
console.log("Updated runner config"); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
console.log("🎉 Deployment complete! 🎉"); | ||||||||||||||||||||||||||||||||
console.log( | ||||||||||||||||||||||||||||||||
"Visit https://" + | ||||||||||||||||||||||||||||||||
FREESTYLE_DOMAIN + | ||||||||||||||||||||||||||||||||
"/ to see your frontend, which is connected to Rivet", | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
function runBuildSteps() { | ||||||||||||||||||||||||||||||||
console.log("Running build steps..."); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
console.log("- Running vite build"); | ||||||||||||||||||||||||||||||||
execSync("vite build", { | ||||||||||||||||||||||||||||||||
stdio: "inherit", | ||||||||||||||||||||||||||||||||
env: { | ||||||||||||||||||||||||||||||||
...process.env, | ||||||||||||||||||||||||||||||||
VITE_RIVET_ENDPOINT: RIVET_ENDPOINT, | ||||||||||||||||||||||||||||||||
VITE_RIVET_NAMESPACE: RIVET_NAMESPACE_NAME, | ||||||||||||||||||||||||||||||||
VITE_RIVET_RUNNER_NAME: RIVET_RUNNER_NAME, | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
console.log("- Running tsup"); | ||||||||||||||||||||||||||||||||
execSync("tsup", { | ||||||||||||||||||||||||||||||||
stdio: "inherit", | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
console.log("Build complete!"); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async function getOrCreateNamespace({ | ||||||||||||||||||||||||||||||||
name, | ||||||||||||||||||||||||||||||||
displayName, | ||||||||||||||||||||||||||||||||
}: { | ||||||||||||||||||||||||||||||||
name: string; | ||||||||||||||||||||||||||||||||
displayName: string; | ||||||||||||||||||||||||||||||||
}): Promise<Rivet.Namespace> { | ||||||||||||||||||||||||||||||||
console.log("- Checking for existing " + name + " namespace"); | ||||||||||||||||||||||||||||||||
const { namespaces } = await rivet.namespaces.list({ | ||||||||||||||||||||||||||||||||
limit: 32, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
const existing = namespaces.find((ns) => ns.name === name); | ||||||||||||||||||||||||||||||||
if (existing) { | ||||||||||||||||||||||||||||||||
console.log("- Found existing namespace " + name); | ||||||||||||||||||||||||||||||||
return existing; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
console.log("- Creating namespace " + name); | ||||||||||||||||||||||||||||||||
const { namespace } = await rivet.namespaces.create({ | ||||||||||||||||||||||||||||||||
displayName, | ||||||||||||||||||||||||||||||||
name, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
return namespace; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async function updateRunnerConfig(namespace: Rivet.Namespace) { | ||||||||||||||||||||||||||||||||
console.log("- Updating runner config for " + RIVET_RUNNER_NAME); | ||||||||||||||||||||||||||||||||
await rivet.runnerConfigs.upsert(RIVET_RUNNER_NAME, { | ||||||||||||||||||||||||||||||||
serverless: { | ||||||||||||||||||||||||||||||||
url: "https://" + FREESTYLE_DOMAIN + "/api/start", | ||||||||||||||||||||||||||||||||
headers: {}, | ||||||||||||||||||||||||||||||||
runnersMargin: 1, | ||||||||||||||||||||||||||||||||
minRunners: 1, | ||||||||||||||||||||||||||||||||
maxRunners: 1, | ||||||||||||||||||||||||||||||||
slotsPerRunner: 1, | ||||||||||||||||||||||||||||||||
requestLifespan: 60 * 4 + 30, | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
namespace: namespace.name, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async function deployToFreestyle() { | ||||||||||||||||||||||||||||||||
console.log("- Deploying to freestyle at https://" + FREESTYLE_DOMAIN); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const buildDir = prepareDirForDeploymentSync( | ||||||||||||||||||||||||||||||||
new URL("../dist", import.meta.url).pathname, | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
if (buildDir.kind === "files") { | ||||||||||||||||||||||||||||||||
buildDir.files["deno.json"] = { | ||||||||||||||||||||||||||||||||
// Fix imports for Deno | ||||||||||||||||||||||||||||||||
content: readFileSync( | ||||||||||||||||||||||||||||||||
new URL("../deno.json", import.meta.url).pathname, | ||||||||||||||||||||||||||||||||
"utf-8", | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
encoding: "utf-8", | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
throw new Error("Expected buildDir to be files"); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
const res = await freestyle.deployWeb(buildDir, { | ||||||||||||||||||||||||||||||||
envVars: { | ||||||||||||||||||||||||||||||||
LOG_LEVEL: "debug", | ||||||||||||||||||||||||||||||||
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`, | ||||||||||||||||||||||||||||||||
RIVET_ENDPOINT, | ||||||||||||||||||||||||||||||||
RIVET_RUNNER_KIND: "serverless", | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
Comment on lines
+137
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deployment configuration is missing the envVars: {
LOG_LEVEL: "debug",
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`,
RIVET_ENDPOINT,
RIVET_RUNNER_KIND: "serverless",
RIVET_PUBLISHABLE_TOKEN: process.env.RIVET_PUBLISHABLE_TOKEN || '',
}, This will ensure the token is properly passed to the Freestyle deployment, maintaining consistency with the documentation.
Suggested change
Spotted by Diamond |
||||||||||||||||||||||||||||||||
timeout: 60 * 5, | ||||||||||||||||||||||||||||||||
entrypoint: "server.cjs", | ||||||||||||||||||||||||||||||||
domains: [FREESTYLE_DOMAIN], | ||||||||||||||||||||||||||||||||
build: false, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
console.log("Deployment id=" + res.deploymentId); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
main(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { actor, setup } from "rivetkit"; | ||
|
||
export type Message = { sender: string; text: string; timestamp: number }; | ||
|
||
export const chatRoom = actor({ | ||
// Persistent state that survives restarts: https://rivet.dev/docs/actors/state | ||
state: { | ||
messages: [] as Message[], | ||
}, | ||
|
||
actions: { | ||
// Callable functions from clients: https://rivet.dev/docs/actors/actions | ||
sendMessage: (c, sender: string, text: string) => { | ||
const message = { sender, text, timestamp: Date.now() }; | ||
// State changes are automatically persisted | ||
c.state.messages.push(message); | ||
// Send events to all connected clients: https://rivet.dev/docs/actors/events | ||
c.broadcast("newMessage", message); | ||
return message; | ||
}, | ||
|
||
getHistory: (c) => c.state.messages, | ||
}, | ||
}); | ||
|
||
// Register actors for use: https://rivet.dev/docs/setup | ||
export const registry = setup({ | ||
use: { chatRoom }, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Hono } from "hono"; | ||
import { serveStatic, upgradeWebSocket } from "hono/deno"; | ||
import { registry } from "./registry"; | ||
|
||
const serverOutput = registry.start({ | ||
NathanFlurry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
inspector: { | ||
enabled: true, | ||
}, | ||
disableDefaultServer: true, | ||
basePath: "/api", | ||
getUpgradeWebSocket: () => upgradeWebSocket, | ||
overrideServerAddress: `${process.env.FREESTYLE_ENDPOINT ?? "http://localhost:8080"}/api`, | ||
cors: { | ||
origin: process.env.FREESTYLE_ENDPOINT ?? "http://localhost:5173", | ||
credentials: true, | ||
}, | ||
}); | ||
|
||
const app = new Hono(); | ||
app.use("/api/*", async (c) => { | ||
return await serverOutput.fetch(c.req.raw); | ||
}); | ||
app.use("*", serveStatic({ root: "./public" })); | ||
|
||
// Under the hood, Freestyle uses Deno | ||
// for their Web Deploy instances | ||
// @ts-ignore | ||
Deno.serve({ port: 8080 }, app.fetch); |
Uh oh!
There was an error while loading. Please reload this page.