diff --git a/.claude/reference/feature-flags.md b/.claude/reference/feature-flags.md
index 05f7d066bd..e805adf184 100644
--- a/.claude/reference/feature-flags.md
+++ b/.claude/reference/feature-flags.md
@@ -28,12 +28,13 @@ if (features.platform) {
| `acl` | Engine enforces token auth on the public endpoint. Implied by `platform`; set independently for enterprise. |
| `billing` | Billing UI. |
| `captcha` | Turnstile captcha on auth forms. Requires `auth`. |
+| `compute` | Rivet Compute (managed pool) UI: namespace deployments + logs sidebar links and routes, actor-details deployment-logs tab, and the Rivet provider option in onboarding / Add Provider. Requires `platform`. |
| `support` | Support/help affordances. |
| `branding` | Rivet branding chrome. |
| `datacenter` | Datacenter-related UI. |
| `danger-zone` | Destructive settings actions (`features.dangerZone`). |
-Deployment flavors map to flag sets roughly as: **cloud** = all on; **OSS** = `auth`/`platform`/`acl` off; **enterprise** = `acl` on, `auth`/`platform` off (engine enforces auth without a login UI). Do not treat `platform`/`auth` as "engine requires credentials" — that is `acl`.
+Deployment flavors map to flag sets roughly as: **cloud** = all on; **OSS** = `auth`/`platform`/`acl` off; **enterprise** = `acl` on, `auth`/`platform` off (engine enforces auth without a login UI). Do not treat `platform`/`auth` as "engine requires credentials" — that is `acl`. **`compute` is opt-in even on cloud** — each Railway service adds it to `VITE_FEATURE_FLAGS` per-environment (e.g. staging on, prod off) rather than inheriting the cloud default-on set.
## When to add a new flag
diff --git a/frontend/src/app/actors-grid.tsx b/frontend/src/app/actors-grid.tsx
index 473da2b2c2..73333ee2c2 100644
--- a/frontend/src/app/actors-grid.tsx
+++ b/frontend/src/app/actors-grid.tsx
@@ -1,4 +1,4 @@
-import { faGear, faPlus, Icon } from "@rivet-gg/icons";
+import { faGear, faLogs, faPlus, Icon } from "@rivet-gg/icons";
import {
queryOptions,
useInfiniteQuery,
@@ -18,6 +18,7 @@ import {
WithTooltip,
} from "@/components";
import { cloudEnv } from "@/lib/env";
+import { features } from "@/lib/features";
import { ActorIcon } from "@/components/lazy-icon";
import { useDataProvider, useCloudNamespaceDataProvider } from "@/components/actors";
import { VisibilitySensor } from "@/components/visibility-sensor";
@@ -81,8 +82,9 @@ export function ActorsGrid({
pool: "default",
safe: true,
}),
+ enabled: features.compute,
});
- const hasCompute = managedPool != null;
+ const hasCompute = features.compute && managedPool != null;
const { data: runnerNamesCount = 0 } = useInfiniteQuery({
...dataProvider.runnerNamesQueryOptions(),
select: (data) => data.pages.flatMap((page) => page.names).length,
@@ -289,12 +291,13 @@ function DeploymentsSection() {
pool: "default",
safe: true,
}),
+ enabled: features.compute,
refetchInterval: (query) =>
query.state.data === null ? false : 5_000,
refetchOnWindowFocus: (query) => query.state.data !== null,
});
- const hasPool = managedPool != null;
+ const hasPool = features.compute && managedPool != null;
const {
data: images,
diff --git a/frontend/src/app/dialogs/upsert-deployment-frame.tsx b/frontend/src/app/dialogs/upsert-deployment-frame.tsx
index 6dfd8123c1..b2f988a709 100644
--- a/frontend/src/app/dialogs/upsert-deployment-frame.tsx
+++ b/frontend/src/app/dialogs/upsert-deployment-frame.tsx
@@ -67,8 +67,7 @@ export default function UpsertDeploymentFrameContent({
displayName: "default",
pool: "default",
image: defaultImage,
- minCount: 0,
- maxCount: 100_000,
+ maxConcurrentActors: 10000,
environment: {},
command: undefined,
args: [],
diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx
index fd093a1e57..3c3ed60104 100644
--- a/frontend/src/app/getting-started.tsx
+++ b/frontend/src/app/getting-started.tsx
@@ -288,7 +288,7 @@ export function GettingStarted({
// after form reset, so read it from the live form.
const provider = (values.provider ?? form.getValues("provider")) as string | undefined;
if (stepper.current.id === "provider") {
- if (provider === "rivet") {
+ if (features.compute && provider === "rivet") {
await mutateAsyncManagedPool({
displayName: "default",
pool: "default",
@@ -319,6 +319,7 @@ export function GettingStarted({
}
if (
stepper.current.id === "backend" &&
+ features.compute &&
provider === "rivet"
) {
return;
@@ -490,6 +491,7 @@ function ProviderSetup() {
const { setValue, control, getValues } = useFormContext();
useEffect(() => {
+ if (!features.compute) return;
if (!getValues("provider")) {
setValue("provider", "rivet", {
shouldDirty: true,
@@ -512,21 +514,32 @@ function ProviderSetup() {
return (
- {deployOptions.map((option) => (
-
- setValue("provider", option.name, {
- shouldDirty: true,
- shouldTouch: true,
- shouldValidate: true,
- })
- }
- className={option.name === "rivet" ? "col-span-2 py-5" : undefined}
- />
- ))}
+ {deployOptions
+ .filter(
+ (option) =>
+ features.compute ||
+ option.name !== "rivet",
+ )
+ .map((option) => (
+
+ setValue("provider", option.name, {
+ shouldDirty: true,
+ shouldTouch: true,
+ shouldValidate: true,
+ })
+ }
+ className={
+ features.compute &&
+ option.name === "rivet"
+ ? "col-span-2 py-5"
+ : undefined
+ }
+ />
+ ))}
);
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 715432e7f9..be2fcb9f99 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -714,8 +714,13 @@ function CloudSidebarContentInner() {
}
function DeploymentsLink() {
+ if (!features.compute) {
+ return null;
+ }
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant
const provider = useCloudNamespaceDataProvider();
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant
const { data } = useSuspenseQuery(
provider.currentNamespaceHasManagedPoolQueryOptions(),
);
diff --git a/frontend/src/app/provider-dropdown.tsx b/frontend/src/app/provider-dropdown.tsx
index 674de76c58..2da21ef703 100644
--- a/frontend/src/app/provider-dropdown.tsx
+++ b/frontend/src/app/provider-dropdown.tsx
@@ -22,6 +22,7 @@ import {
} from "@/components";
import { useEngineCompatDataProvider } from "@/components/actors";
import { deriveProviderFromMetadata } from "@/lib/data";
+import { features } from "@/lib/features";
export function ProviderDropdown({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
@@ -101,7 +102,7 @@ export function ProviderDropdown({ children }: { children: React.ReactNode }) {
{children}
-
+ {features.compute ? : null}
{externalClouds}
diff --git a/frontend/src/components/actors/actors-actor-details.tsx b/frontend/src/components/actors/actors-actor-details.tsx
index 08f769d0f3..775a12bd91 100644
--- a/frontend/src/components/actors/actors-actor-details.tsx
+++ b/frontend/src/components/actors/actors-actor-details.tsx
@@ -116,7 +116,7 @@ const TAB_PRIORITY = [
type TabId = (typeof TAB_PRIORITY)[number];
function useManagedPool() {
- if (!features.platform) return false;
+ if (!features.compute) return false;
// biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant
const provider = useCloudNamespaceDataProvider();
// biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant
diff --git a/frontend/src/lib/features.ts b/frontend/src/lib/features.ts
index 5cc7b38136..bd46bfa559 100644
--- a/frontend/src/lib/features.ts
+++ b/frontend/src/lib/features.ts
@@ -26,6 +26,10 @@ export const features = {
acl,
billing: isEnabled("billing"),
captcha: isEnabled("captcha") && auth,
+ // `compute` gates the Rivet Compute (managed pool) UI: namespace
+ // deployments, logs, and the Rivet provider option. Cloud-platform-only
+ // because every surface consumes cloud-namespace data providers.
+ compute: isEnabled("compute") && platform,
support: isEnabled("support"),
branding: isEnabled("branding"),
datacenter: isEnabled("datacenter"),
diff --git a/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/deployments.tsx b/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/deployments.tsx
index ddac024cb0..24a9ddcf14 100644
--- a/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/deployments.tsx
+++ b/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/deployments.tsx
@@ -4,16 +4,25 @@ import {
useSuspenseInfiniteQuery,
useSuspenseQuery,
} from "@tanstack/react-query";
-import { createFileRoute, Link } from "@tanstack/react-router";
+import { createFileRoute, Link, redirect } from "@tanstack/react-router";
import { ImagesTable } from "@/app/images-table";
import { Content } from "@/app/layout";
import { Button, H1, H2, Skeleton } from "@/components";
import { useCloudNamespaceDataProvider } from "@/components/actors";
+import { features } from "@/lib/features";
export const Route = createFileRoute(
"/_context/orgs/$organization/projects/$project/ns/$namespace/deployments",
)({
component: RouteComponent,
+ beforeLoad: ({ params }) => {
+ if (!features.compute) {
+ throw redirect({
+ to: "/orgs/$organization/projects/$project/ns/$namespace",
+ params,
+ });
+ }
+ },
loader: async ({ context }) => {
const dataProvider = context.dataProvider;
await context.queryClient.prefetchQuery(
diff --git a/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/logs.tsx b/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/logs.tsx
index 5362e18d77..3e04de9c9d 100644
--- a/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/logs.tsx
+++ b/frontend/src/routes/_context/orgs.$organization/projects.$project/ns.$namespace/logs.tsx
@@ -7,7 +7,7 @@ import {
Icon,
} from "@rivet-gg/icons";
import { useInfiniteQuery, useSuspenseQuery } from "@tanstack/react-query";
-import { createFileRoute, Link } from "@tanstack/react-router";
+import { createFileRoute, Link, redirect } from "@tanstack/react-router";
import { startTransition, useCallback, useRef, useState } from "react";
import { z } from "zod";
import { Content } from "@/app/layout";
@@ -24,6 +24,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { features } from "@/lib/features";
export const Route = createFileRoute(
"/_context/orgs/$organization/projects/$project/ns/$namespace/logs",
@@ -32,6 +33,14 @@ export const Route = createFileRoute(
search: z.string().optional(),
}),
component: RouteComponent,
+ beforeLoad: ({ params }) => {
+ if (!features.compute) {
+ throw redirect({
+ to: "/orgs/$organization/projects/$project/ns/$namespace",
+ params,
+ });
+ }
+ },
loader: async ({ context }) => {
const dataProvider = context.dataProvider;
await context.queryClient.prefetchQuery(
diff --git a/website/src/content/docs/connect/rivet-compute.mdx b/website/src/content/docs/connect/rivet-compute.mdx
index c7c1c307ec..f755a9a65b 100644
--- a/website/src/content/docs/connect/rivet-compute.mdx
+++ b/website/src/content/docs/connect/rivet-compute.mdx
@@ -5,7 +5,7 @@ skill: true
---
-Rivet Cloud is currently in beta.
+Rivet Compute is currently in beta.
@@ -22,14 +22,23 @@ Using an AI coding agent? Open **Connect** on the [Rivet dashboard](https://dash
- A [Rivet Cloud](https://dashboard.rivet.dev) account and project
-
+
-Rivet Compute runs your app as a long-lived container. Make sure your server calls `startRunner()` instead of `serve()`:
+Rivet Compute runs your app as a short-lived, serverless container. Make sure your server `serve()` or uses `handler()` instead of `startRunner()`:
```typescript src/server.js @nocheck
import { registry } from "./actors.js";
+import { Hono } from "hono";
+import { serve } from "@hono/node-server";
-registry.startRunner();
+const app = new Hono();
+
+// Mount Rivet handler
+app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
+
+const PORT = parseInt(process.env.PORT);
+
+serve({ fetch: app.fetch, port: PORT });
```
See [Runtime Modes](/docs/general/runtime-modes) for details on when to use each mode.
@@ -147,6 +156,7 @@ If the status shows **Error**, check that your container starts successfully and
- The server file is not calling `registry.startRunner()`
- A runtime crash on startup — test the image locally with `docker run`
+- The Dockerfile is not listening on the `PORT` environmental variable