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