+ Overview
+
+
- Connect
+ Tokens
{hasDataProvider && hasQuery ? (
diff --git a/frontend/src/app/publishable-token-code-group.tsx b/frontend/src/app/publishable-token-code-group.tsx
new file mode 100644
index 0000000000..fd6bc9fe1a
--- /dev/null
+++ b/frontend/src/app/publishable-token-code-group.tsx
@@ -0,0 +1,165 @@
+import { faChevronRight, faNextjs, faNodeJs, faReact, Icon } from "@rivet-gg/icons";
+import { useInfiniteQuery } from "@tanstack/react-query";
+import { hasProvider } from "@/app/data-providers/engine-data-provider";
+import { CodeFrame, CodeGroup, CodePreview, DocsSheet } from "@/components";
+import { useEngineCompatDataProvider } from "@/components/actors";
+
+interface PublishableTokenCodeGroupProps {
+ token: string;
+ endpoint: string;
+ namespace: string;
+}
+
+export function PublishableTokenCodeGroup({
+ token,
+ endpoint,
+ namespace,
+}: PublishableTokenCodeGroupProps) {
+ const dataProvider = useEngineCompatDataProvider();
+ const { data: configs } = useInfiniteQuery({
+ ...dataProvider.runnerConfigsQueryOptions(),
+ refetchInterval: 5000,
+ });
+
+ // Check if Vercel is connected
+ const hasVercel = hasProvider(configs, ["vercel", "next-js"]);
+
+ const nextJsTab = (
+
+
+ See Next.js Documentation{" "}
+
+
+
+ }
+ >
+
+
+ );
+
+ const reactTab = (
+
+
+ See React Documentation{" "}
+
+
+
+ }
+ >
+
+
+ );
+
+ const javascriptTab = (
+
+
+ See JavaScript Documentation{" "}
+
+
+
+ }
+ >
+
+
+ );
+
+ return (
+
+ {hasVercel
+ ? [nextJsTab, reactTab, javascriptTab]
+ : [javascriptTab, reactTab, nextJsTab]}
+
+ );
+}
+
+const javascriptCode = ({
+ token,
+ endpoint,
+ namespace,
+}: {
+ token: string;
+ endpoint: string;
+ namespace: string;
+}) => `import { createClient } from "rivetkit/client";
+import type { registry } from "./registry";
+
+const client = createClient
({
+ endpoint: "${endpoint}",
+ namespace: "${namespace}",
+ // This token is safe to publish on your frontend
+ token: "${token}",
+});`;
+
+const reactCode = ({
+ token,
+ endpoint,
+ namespace,
+}: {
+ token: string;
+ endpoint: string;
+ namespace: string;
+}) => `import { createRivetKit } from "@rivetkit/react";
+import type { registry } from "./registry";
+
+export const { useActor } = createRivetKit({
+ endpoint: "${endpoint}",
+ namespace: "${namespace}",
+ // This token is safe to publish on your frontend
+ token: "${token}",
+});`;
+
+const nextJsCode = ({
+ token,
+ endpoint,
+ namespace,
+}: {
+ token: string;
+ endpoint: string;
+ namespace: string;
+}) => `"use client";
+import { createRivetKit } from "@rivetkit/next-js/client";
+import type { registry } from "@/rivet/registry";
+
+export const { useActor } = createRivetKit({
+ endpoint: "${endpoint}",
+ namespace: "${namespace}",
+ // This token is safe to publish on your frontend
+ token: "${token}",
+});`;
diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx
index 9918da25bc..14f84484b7 100644
--- a/frontend/src/app/use-dialog.tsx
+++ b/frontend/src/app/use-dialog.tsx
@@ -43,4 +43,8 @@ export const useDialog = {
() => import("@/app/dialogs/provide-engine-credentials-frame"),
),
Tokens: createDialogHook(() => import("@/app/dialogs/tokens-frame")),
+ ApiTokens: createDialogHook(() => import("@/app/dialogs/api-tokens-frame")),
+ CreateApiToken: createDialogHook(
+ () => import("@/app/dialogs/create-api-token-frame"),
+ ),
};
diff --git a/frontend/src/app/user-dropdown.tsx b/frontend/src/app/user-dropdown.tsx
index 1e8d7a70fc..59e31fe99e 100644
--- a/frontend/src/app/user-dropdown.tsx
+++ b/frontend/src/app/user-dropdown.tsx
@@ -65,18 +65,6 @@ export function UserDropdown() {
>
Profile
- {isMatchingNamespaceRoute ? (
- {
- navigate({
- to: ".",
- search: { ...params, modal: "tokens" },
- });
- }}
- >
- Tokens
-
- ) : null}
{clerk.organization ? (
{
diff --git a/frontend/src/components/actors/data-provider.tsx b/frontend/src/components/actors/data-provider.tsx
index f1287b6577..7659d5c107 100644
--- a/frontend/src/components/actors/data-provider.tsx
+++ b/frontend/src/components/actors/data-provider.tsx
@@ -7,8 +7,8 @@ import {
} from "@tanstack/react-router";
import { match } from "ts-pattern";
-export const useDataProvider = () =>
- match(__APP_TYPE__)
+export const useDataProvider = () => {
+ return match(__APP_TYPE__)
.with("cloud", () => {
// biome-ignore lint/correctness/useHookAtTopLevel: runs only once
return useRouteContext({
@@ -36,6 +36,7 @@ export const useDataProvider = () =>
});
})
.exhaustive();
+};
export const useDataProviderCheck = () => {
const matchRoute = useMatchRoute();
@@ -104,16 +105,19 @@ export const useCloudNamespaceDataProvider = () => {
};
export const useEngineCompatDataProvider = () => {
+ const routePath = match(__APP_TYPE__)
+ .with("cloud", () => {
+ return "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace" as const;
+ })
+ .with("engine", () => {
+ return "/_context/_engine/ns/$namespace" as const;
+ })
+ .otherwise(() => {
+ throw new Error("Not in an engine-like context");
+ });
+
return useRouteContext({
- from: match(__APP_TYPE__)
- .with("cloud", () => {
- return "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace" as const;
- })
- .with("engine", () => {
- return "/_context/_engine/ns/$namespace" as const;
- })
- .otherwise(() => {
- throw new Error("Not in an engine-like context");
- }),
+ from: routePath,
+ strict: false,
}).dataProvider;
};
diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts
index 7479ac003d..2875d428ab 100644
--- a/frontend/src/queries/utils.ts
+++ b/frontend/src/queries/utils.ts
@@ -2,7 +2,7 @@ import type { Query } from "@tanstack/react-query";
export const shouldRetryAllExpect403 = (failureCount: number, error: Error) => {
if (error && "statusCode" in error) {
- if (error.statusCode === 403) {
+ if (error.statusCode === 403 || error.statusCode === 401) {
// Don't retry on auth errors, when app is not engine
return __APP_TYPE__ !== "engine";
}
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 70ac67d4b7..619fe8d6fb 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -29,6 +29,7 @@ import { Route as ContextCloudOrgsOrganizationProjectsProjectRouteImport } from
import { Route as ContextCloudOrgsOrganizationProjectsProjectIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/index'
import { Route as ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace'
import { Route as ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/index'
+import { Route as ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/tokens'
import { Route as ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect'
const SsoCallbackRoute = SsoCallbackRouteImport.update({
@@ -143,6 +144,15 @@ const ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute =
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRoute,
} as any,
)
+const ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute =
+ ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRouteImport.update(
+ {
+ id: '/tokens',
+ path: '/tokens',
+ getParentRoute: () =>
+ ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRoute,
+ } as any,
+ )
const ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute =
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRouteImport.update(
{
@@ -171,6 +181,7 @@ export interface FileRoutesByFullPath {
'/orgs/$organization/projects/$project/': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute
'/orgs/$organization/projects/$project/ns/$namespace': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteWithChildren
'/orgs/$organization/projects/$project/ns/$namespace/connect': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute
+ '/orgs/$organization/projects/$project/ns/$namespace/tokens': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute
'/orgs/$organization/projects/$project/ns/$namespace/': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute
}
export interface FileRoutesByTo {
@@ -187,6 +198,7 @@ export interface FileRoutesByTo {
'/orgs/$organization/projects': typeof ContextCloudOrgsOrganizationProjectsIndexRoute
'/orgs/$organization/projects/$project': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute
'/orgs/$organization/projects/$project/ns/$namespace/connect': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute
+ '/orgs/$organization/projects/$project/ns/$namespace/tokens': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute
'/orgs/$organization/projects/$project/ns/$namespace': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute
}
export interface FileRoutesById {
@@ -211,6 +223,7 @@ export interface FileRoutesById {
'/_context/_cloud/orgs/$organization/projects/$project/': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute
'/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteWithChildren
'/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute
+ '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/tokens': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute
'/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute
}
export interface FileRouteTypes {
@@ -233,6 +246,7 @@ export interface FileRouteTypes {
| '/orgs/$organization/projects/$project/'
| '/orgs/$organization/projects/$project/ns/$namespace'
| '/orgs/$organization/projects/$project/ns/$namespace/connect'
+ | '/orgs/$organization/projects/$project/ns/$namespace/tokens'
| '/orgs/$organization/projects/$project/ns/$namespace/'
fileRoutesByTo: FileRoutesByTo
to:
@@ -249,6 +263,7 @@ export interface FileRouteTypes {
| '/orgs/$organization/projects'
| '/orgs/$organization/projects/$project'
| '/orgs/$organization/projects/$project/ns/$namespace/connect'
+ | '/orgs/$organization/projects/$project/ns/$namespace/tokens'
| '/orgs/$organization/projects/$project/ns/$namespace'
id:
| '__root__'
@@ -272,6 +287,7 @@ export interface FileRouteTypes {
| '/_context/_cloud/orgs/$organization/projects/$project/'
| '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace'
| '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect'
+ | '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/tokens'
| '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/'
fileRoutesById: FileRoutesById
}
@@ -425,6 +441,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRouteImport
parentRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRoute
}
+ '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/tokens': {
+ id: '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/tokens'
+ path: '/tokens'
+ fullPath: '/orgs/$organization/projects/$project/ns/$namespace/tokens'
+ preLoaderRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRouteImport
+ parentRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRoute
+ }
'/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect': {
id: '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect'
path: '/connect'
@@ -437,6 +460,7 @@ declare module '@tanstack/react-router' {
interface ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteChildren {
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute
+ ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute: typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute
}
@@ -444,6 +468,8 @@ const ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteChildren: Conte
{
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute:
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute,
+ ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute:
+ ContextCloudOrgsOrganizationProjectsProjectNsNamespaceTokensRoute,
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute:
ContextCloudOrgsOrganizationProjectsProjectNsNamespaceIndexRoute,
}
diff --git a/frontend/src/routes/_context/_cloud.tsx b/frontend/src/routes/_context/_cloud.tsx
index b8b5953f0b..d9779d4d27 100644
--- a/frontend/src/routes/_context/_cloud.tsx
+++ b/frontend/src/routes/_context/_cloud.tsx
@@ -47,7 +47,6 @@ function CloudModals() {
const ConnectHetznerDialog = useDialog.ConnectHetzner.Dialog;
const EditProviderConfigDialog = useDialog.EditProviderConfig.Dialog;
const DeleteConfigDialog = useDialog.DeleteConfig.Dialog;
- const TokensDialog = useDialog.Tokens.Dialog;
return (
<>
@@ -245,23 +244,6 @@ function CloudModals() {
},
}}
/>
- {
- if (!value) {
- navigate({
- to: ".",
- search: (old) => ({
- ...old,
- modal: undefined,
- }),
- });
- }
- },
- }}
- />
data.pages[0].names.length,
@@ -78,20 +76,15 @@ export function RouteComponent() {
-
Connect
-
-
- Tokens
-
-
- }
- >
- Need help?
-
-
-
+
Overview
+
+ }
+ >
+ Need help?
+
+
Connect your RivetKit application to Rivet Cloud. Use
@@ -197,21 +190,14 @@ export function RouteComponent() {
Connect Existing Project
-
-
- Tokens
-
-
-
- }
- >
- Need help?
-
-
-
+
+ }
+ >
+ Need help?
+
+
Connect your RivetKit application to Rivet Cloud.
@@ -320,20 +306,15 @@ export function RouteComponent() {
-
Connect
-
-
- Tokens
-
-
- }
- >
- Need help?
-
-
-
+
Overview
+
+ }
+ >
+ Need help?
+
+
Connect your RivetKit application to Rivet Cloud. Use
@@ -367,7 +348,7 @@ function Providers() {
return (
-
+
Providers
@@ -475,90 +456,6 @@ function ConnectYourFrontend() {
const dataProvider = useEngineCompatDataProvider();
const namespace = dataProvider.engineNamespace;
- const { data: configs } = useInfiniteQuery({
- ...dataProvider.runnerConfigsQueryOptions(),
- refetchInterval: 5000,
- });
-
- // Check if Vercel is connected
- const hasVercel = hasProvider(configs, ["vercel", "next-js"]);
-
- const nextJsTab = (
-
-
- See Next.js Documentation{" "}
-
-
-
- }
- >
-
-
- );
-
- const reactTab = (
-
-
- See React Documentation{" "}
-
-
-
- }
- >
-
-
- );
-
- const javascriptTab = (
-
-
- See JavaScript Documentation{" "}
-
-
-
- }
- >
-
-
- );
-
return (
@@ -568,69 +465,16 @@ function ConnectYourFrontend() {
This token is safe to publish on your frontend.
-
- {hasVercel
- ? [nextJsTab, reactTab, javascriptTab]
- : [javascriptTab, reactTab, nextJsTab]}
-
+
);
}
-const javascriptCode = ({
- token,
- endpoint,
- namespace,
-}: {
- token: string;
- endpoint: string;
- namespace: string;
-}) => `import { createClient } from "rivetkit/client";
-import type { registry } from "./registry";
-
-const client = createClient
({
- endpoint: "${endpoint}",
- namespace: "${namespace}",
- token: "${token}",
-});`;
-
-const reactCode = ({
- token,
- endpoint,
- namespace,
-}: {
- token: string;
- endpoint: string;
- namespace: string;
-}) => `import { createRivetKit } from "@rivetkit/react";
-import type { registry } from "./registry";
-
-export const { useActor } = createRivetKit({
- endpoint: "${endpoint}",
- namespace: "${namespace}",
- token: "${token}",
-});`;
-
-const nextJsCode = ({
- token,
- endpoint,
- namespace,
-}: {
- token: string;
- endpoint: string;
- namespace: string;
-}) => `"use client";
-import { createRivetKit } from "@rivetkit/next-js/client";
-import type { registry } from "@/rivet/registry";
-
-export const { useActor } = createRivetKit({
- endpoint: "${engineEnv().VITE_APP_API_URL}",
- namespace: "${namespace}",
- token: "${token}",
-});
-`;
-
function ProviderDropdown({ children }: { children: React.ReactNode }) {
const navigate = Route.useNavigate();
return (
diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/tokens.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/tokens.tsx
new file mode 100644
index 0000000000..2c199404c2
--- /dev/null
+++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/tokens.tsx
@@ -0,0 +1,546 @@
+import { faChevronRight, faCopy, faNodeJs, faPlus, faQuestionCircle, faTrash, Icon } from "@rivet-gg/icons";
+import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
+import { createFileRoute, useParams, useRouteContext } from "@tanstack/react-router";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { match } from "ts-pattern";
+import { HelpDropdown } from "@/app/help-dropdown";
+import { PublishableTokenCodeGroup } from "@/app/publishable-token-code-group";
+import { useDialog } from "@/app/use-dialog";
+import {
+ Badge,
+ Button,
+ CodeFrame,
+ CodeGroup,
+ CodePreview,
+ DiscreteInput,
+ DocsSheet,
+ getConfig,
+ H1,
+ H3,
+ Label,
+ Skeleton,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components";
+import { useEngineCompatDataProvider } from "@/components/actors";
+import { RegionSelect } from "@/components/actors/region-select";
+import { cloudEnv } from "@/lib/env";
+import { queryClient } from "@/queries/global";
+
+export const Route = createFileRoute(
+ "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/tokens",
+)({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return (
+
+
+
+
Tokens
+
+ }
+ >
+ Need help?
+
+
+
+
+ These tokens are used to authenticate your app with Rivet.
+
+
+
+
+
+ );
+}
+
+function PublishableToken() {
+ const dataProvider = useEngineCompatDataProvider();
+ const { data: token, isLoading } = useQuery(
+ dataProvider.publishableTokenQueryOptions(),
+ );
+
+ const namespace = dataProvider.engineNamespace;
+
+ const endpoint = match(__APP_TYPE__)
+ .with("cloud", () => cloudEnv().VITE_APP_API_URL)
+ .with("engine", () => getConfig().apiUrl)
+ .otherwise(() => {
+ throw new Error("Not in a valid context");
+ });
+
+ return (
+
+
+
Client Token
+
+
+ Connect to your actors using the Rivet client token. This can be used either on your frontend or backend.
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ {token && (
+
+ )}
+
+
+ );
+}
+
+function SecretToken() {
+ const dataProvider = useEngineCompatDataProvider();
+ const { data: token, isLoading: isTokenLoading } = useQuery(
+ dataProvider.engineAdminTokenQueryOptions(),
+ );
+ const { data: regions = [] } = useInfiniteQuery(
+ dataProvider.regionsQueryOptions(),
+ );
+ const [selectedDatacenter, setSelectedDatacenter] = useState<
+ string | undefined
+ >(undefined);
+
+ // Set default datacenter when regions are loaded
+ useEffect(() => {
+ if (regions.length > 0 && !selectedDatacenter) {
+ setSelectedDatacenter(regions[0].id);
+ }
+ }, [regions, selectedDatacenter]);
+
+ const namespace = dataProvider.engineNamespace;
+
+ const endpoint = match(__APP_TYPE__)
+ .with("cloud", () => {
+ const region = regions.find((r) => r.id === selectedDatacenter);
+ return region?.endpoint || cloudEnv().VITE_APP_API_URL;
+ })
+ .with("engine", () => getConfig().apiUrl)
+ .otherwise(() => {
+ throw new Error("Not in a valid context");
+ });
+
+ const codeSnippet = `import { registry } from "./registry";
+
+// Automatically reads token from env
+registry.start();`;
+
+ return (
+
+
+
Runner Token
+
+
+ Used by runners (servers that run your actors) to authenticate
+ with Rivet. Serverless providers do not need to use this token.
+
+
+
+ Datacenter
+
+
+
+
Environment Variables
+
+
+ Key
+
+
+ Value
+
+
+
+
+
+
+ {isTokenLoading ? (
+
+ ) : (
+
+ )}
+
+
+ {
+ const envVars = `RIVET_ENDPOINT=${endpoint}
+RIVET_NAMESPACE=${namespace}
+RIVET_TOKEN=${token || ""}`;
+ navigator.clipboard.writeText(envVars);
+ toast.success("Copied to clipboard");
+ }}
+ >
+ Copy all raw
+
+
+
+
+ {[
+ codeSnippet}
+ footer={
+
+
+ See JavaScript Documentation{" "}
+
+
+
+ }
+ >
+
+
+ ]}
+
+
+
+ );
+}
+
+function CloudApiTokens() {
+ const { dataProvider } = useRouteContext({
+ from: "/_context/_cloud/orgs/$organization/projects/$project",
+ });
+ const params = useParams({ strict: false });
+ const organization = params.organization;
+ const project = params.project;
+ const namespace = params.namespace;
+
+ const { data, isLoading } = useQuery(dataProvider.apiTokensQueryOptions());
+
+ const { open: openCreateApiToken, dialog: createApiTokenDialog } =
+ useDialog.CreateApiToken({});
+
+ const cloudApiUrl = cloudEnv().VITE_APP_CLOUD_API_URL;
+
+ return (
+
+
+
+
Cloud API Tokens
+ Beta
+
+
openCreateApiToken()}
+ startIcon={ }
+ >
+ Create API Token
+
+
+
+ Cloud API tokens provide programmatic access to the Rivet Cloud API. Keep
+ them secure and never share them publicly.
+
+
+ {isLoading ? (
+
+
+
+
+
+ ) : (
+
+
+
+ Name
+ Token
+ Created
+ Expires
+
+
+
+
+ {data?.apiTokens.length === 0 ? (
+
+
+ No Cloud API tokens yet. Create one to get started.
+
+
+ ) : (
+ data?.apiTokens.map((apiToken) => (
+
+ ))
+ )}
+
+
+ )}
+
+
+
+ `const response = await fetch("${cloudApiUrl}/projects/${project}/namespaces?org=${organization}", {
+ method: "POST",
+ headers: {
+ "Authorization": "Bearer \${YOUR_CLOUD_API_TOKEN}",
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ displayName: "my-namespace"
+ })
+});
+
+const data = await response.json();
+console.log(data.namespace);`}
+ >
+
+
+ `const response = await fetch("${cloudApiUrl}/projects/${project}/namespaces?org=${organization}&limit=10", {
+ method: "GET",
+ headers: {
+ "Authorization": "Bearer \${YOUR_CLOUD_API_TOKEN}"
+ }
+});
+
+const data = await response.json();
+console.log(data.namespaces);`}
+ >
+
+
+ `const response = await fetch("${cloudApiUrl}/projects/${project}/namespaces/${namespace}?org=${organization}", {
+ method: "GET",
+ headers: {
+ "Authorization": "Bearer \${YOUR_CLOUD_API_TOKEN}"
+ }
+});
+
+const data = await response.json();
+console.log(data.namespace);`}
+ >
+
+
+ `const response = await fetch("${cloudApiUrl}/projects/${project}/namespaces/${namespace}/tokens/secret?org=${organization}", {
+ method: "POST",
+ headers: {
+ "Authorization": "Bearer \${YOUR_CLOUD_API_TOKEN}"
+ }
+});
+
+const data = await response.json();
+console.log(data.token);`}
+ >
+
+
+ `const response = await fetch("${cloudApiUrl}/projects/${project}/namespaces/${namespace}/tokens/publishable?org=${organization}", {
+ method: "POST",
+ headers: {
+ "Authorization": "Bearer \${YOUR_CLOUD_API_TOKEN}"
+ }
+});
+
+const data = await response.json();
+console.log(data.token);`}
+ >
+
+
+
+
+ {createApiTokenDialog}
+
+ );
+}
+
+interface ApiTokenRowProps {
+ apiToken: {
+ id: string;
+ name: string;
+ createdAt: string;
+ expiresAt?: string;
+ revoked: boolean;
+ lastFourChars: string;
+ };
+ dataProvider: ReturnType<
+ typeof useRouteContext<"/_context/_cloud/orgs/$organization/projects/$project">
+ >["dataProvider"];
+}
+
+function ApiTokenRow({ apiToken, dataProvider }: ApiTokenRowProps) {
+ const { mutate: revoke, isPending } = useMutation(
+ dataProvider.revokeApiTokenMutationOptions({
+ onSuccess: async () => {
+ await queryClient.invalidateQueries(
+ dataProvider.apiTokensQueryOptions(),
+ );
+ },
+ }),
+ );
+
+ const createdDate = new Date(apiToken.createdAt).toLocaleDateString();
+ const expiresDate = apiToken.expiresAt
+ ? new Date(apiToken.expiresAt).toLocaleDateString()
+ : "Never";
+
+ return (
+
+ {apiToken.name}
+
+
+ cloud_api_...{apiToken.lastFourChars}
+
+
+ {createdDate}
+ {expiresDate}
+
+ {!apiToken.revoked && (
+ {
+ revoke({ apiTokenId: apiToken.id });
+ }}
+ >
+
+ Revoke
+
+ )}
+ {apiToken.revoked && (
+ Revoked
+ )}
+
+
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 500a09418c..b1a43c6f47 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1637,8 +1637,8 @@ importers:
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@rivet-gg/cloud':
- specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@1fcfb72
- version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@1fcfb72
+ specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c
+ version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c
'@rivet-gg/icons':
specifier: workspace:*
version: link:packages/icons
@@ -6417,8 +6417,8 @@ packages:
'@rivet-gg/api@25.5.3':
resolution: {integrity: sha512-pj8xYQ+I/aQDbThmicPxvR+TWAzGoLSE53mbJi4QZHF8VH2oMvU7CMWqy7OTFH30DIRyVzsnHHRJZKGwtmQL3g==}
- '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@1fcfb72':
- resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@1fcfb72}
+ '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c':
+ resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c}
version: 0.0.0
'@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@bf2ebb2':
@@ -18263,7 +18263,7 @@ snapshots:
transitivePeerDependencies:
- encoding
- '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@1fcfb72':
+ '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c':
dependencies:
cross-fetch: 4.1.0
form-data: 4.0.5