Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/freestyle/.env.sample
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"
2 changes: 2 additions & 0 deletions examples/freestyle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.actorcore
node_modules
65 changes: 65 additions & 0 deletions examples/freestyle/README.md
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
11 changes: 11 additions & 0 deletions examples/freestyle/deno.json
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"
}
}
38 changes: 38 additions & 0 deletions examples/freestyle/package.json
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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency dotenv is specified with version ^17.2.2, but this version doesn't exist in the npm registry. The latest major version of dotenv is 16.x. Consider updating to a valid version such as ^16.3.1 to prevent installation failures when running npm install or pnpm install.

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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"
}
153 changes: 153 additions & 0 deletions examples/freestyle/scripts/freestyle-deploy.ts
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";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runner name freestyle defined here doesn't match the default freestyle-runner specified in both the .env.sample file and the frontend code. This inconsistency could lead to connection issues between the frontend and backend. Consider standardizing on one name (preferably freestyle-runner) across all files to ensure reliable connections.

Suggested change
const RIVET_RUNNER_NAME = "freestyle";
const RIVET_RUNNER_NAME = "freestyle-runner";

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deployment configuration is missing the RIVET_PUBLISHABLE_TOKEN environment variable that's referenced in the README as required for connecting to Rivet Actors. Consider adding it to the envVars object:

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
const res = await freestyle.deployWeb(buildDir, {
envVars: {
LOG_LEVEL: "debug",
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`,
RIVET_ENDPOINT,
RIVET_RUNNER_KIND: "serverless",
},
const res = await freestyle.deployWeb(buildDir, {
envVars: {
LOG_LEVEL: "debug",
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`,
RIVET_ENDPOINT,
RIVET_RUNNER_KIND: "serverless",
RIVET_PUBLISHABLE_TOKEN: process.env.RIVET_PUBLISHABLE_TOKEN || '',
},

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

timeout: 60 * 5,
entrypoint: "server.cjs",
domains: [FREESTYLE_DOMAIN],
build: false,
});

console.log("Deployment id=" + res.deploymentId);
}

main();
29 changes: 29 additions & 0 deletions examples/freestyle/src/backend/registry.ts
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 },
});
28 changes: 28 additions & 0 deletions examples/freestyle/src/backend/server.ts
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({
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);
Loading
Loading