diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx
index fc9ce24d37..8464de51ce 100644
--- a/frontend/src/routes/login.tsx
+++ b/frontend/src/routes/login.tsx
@@ -1,14 +1,26 @@
-import { createFileRoute } from "@tanstack/react-router";
+import { createFileRoute, redirect } from "@tanstack/react-router";
+import { toast } from "sonner";
+import { z } from "zod";
import { Login } from "@/app/login";
import { Logo } from "@/app/logo";
import { authClient, redirectToOrganization } from "@/lib/auth";
export const Route = createFileRoute("/login")({
component: RouteComponent,
+ validateSearch: z.object({ emailVerified: z.coerce.number().optional() }),
beforeLoad: async ({ search }) => {
+ if (search.emailVerified) {
+ toast.success("Email verified successfully. You can now sign in.", {
+ position: "top-center",
+ });
+ throw redirect({ to: ".", search: { emailVerified: undefined } });
+ }
+
const session = await authClient.getSession();
if (session.data) {
- await redirectToOrganization(search as Record);
+ await redirectToOrganization({
+ from: "from" in search ? (search.from as string) : undefined,
+ });
}
},
});
@@ -16,7 +28,7 @@ export const Route = createFileRoute("/login")({
function RouteComponent() {
return (
-
+
diff --git a/frontend/src/routes/onboarding.tsx b/frontend/src/routes/onboarding.tsx
index 3797cd9b66..de52ced2f5 100644
--- a/frontend/src/routes/onboarding.tsx
+++ b/frontend/src/routes/onboarding.tsx
@@ -1,10 +1,11 @@
import { createFileRoute, notFound, Outlet, redirect } from "@tanstack/react-router";
import { authClient } from "@/lib/auth";
+import { features } from "@/lib/features";
export const Route = createFileRoute("/onboarding")({
component: RouteComponent,
beforeLoad: async () => {
- if (__APP_TYPE__ !== "cloud") {
+ if (!features.auth) {
throw notFound();
}
@@ -16,5 +17,5 @@ export const Route = createFileRoute("/onboarding")({
});
function RouteComponent() {
- return __APP_TYPE__ === "cloud" ? : null;
+ return features.auth ? : null;
}
diff --git a/frontend/src/routes/onboarding/choose-organization.tsx b/frontend/src/routes/onboarding/choose-organization.tsx
deleted file mode 100644
index 2bee14fe45..0000000000
--- a/frontend/src/routes/onboarding/choose-organization.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { createFileRoute, redirect } from "@tanstack/react-router";
-import { Content } from "@/app/layout";
-import { RouteLayout } from "@/app/route-layout";
-import { authClient } from "@/lib/auth";
-
-export const Route = createFileRoute("/onboarding/choose-organization")({
- component: RouteComponent,
- beforeLoad: async () => {
- const session = await authClient.getSession();
- if (!session.data) {
- throw redirect({ to: "/login" });
- }
-
- const orgs = await authClient.organization.list();
-
- if (orgs.data && orgs.data.length > 0) {
- await authClient.organization.setActive({
- organizationId: orgs.data[0].id,
- });
- throw redirect({
- to: "/orgs/$organization",
- params: { organization: orgs.data[0].id },
- search: true,
- });
- }
-
- // No orgs — auto-create a default org
- const user = session.data.user;
- const name = `${user.name || user.email.split("@")[0] || "Anonymous"}'s Organization`;
- const slug = `${name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}-${Math.random().toString(36).substring(2, 6)}`;
-
- const newOrg = await authClient.organization.create({ name, slug });
-
- if (newOrg.data) {
- await authClient.organization.setActive({
- organizationId: newOrg.data.id,
- });
- throw redirect({
- to: "/orgs/$organization",
- params: { organization: newOrg.data.id },
- search: true,
- });
- }
-
- // Fallback — should not happen
- throw redirect({ to: "/login" });
- },
-});
-
-function RouteComponent() {
- return (
-
-
-
- Creating your organization...
-
-
-
- );
-}
diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx
new file mode 100644
index 0000000000..bfa82b1e6e
--- /dev/null
+++ b/frontend/src/routes/reset-password.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { Logo } from "@/app/logo";
+import { ResetPassword } from "@/app/reset-password";
+
+export const Route = createFileRoute("/reset-password")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/verify-email-pending.tsx b/frontend/src/routes/verify-email-pending.tsx
new file mode 100644
index 0000000000..e205af5b70
--- /dev/null
+++ b/frontend/src/routes/verify-email-pending.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute, redirect } from "@tanstack/react-router";
+import { z } from "zod";
+import { VerifyEmailPending } from "@/app/verify-email-pending";
+import { authClient } from "@/lib/auth";
+import { features } from "@/lib/features";
+
+export const Route = createFileRoute("/verify-email-pending")({
+ component: VerifyEmailPending,
+ validateSearch: z.object({ email: z.string().optional() }),
+ beforeLoad: async ({ search }) => {
+ if (!features.auth) return;
+
+ const session = await authClient.getSession();
+
+ // No session and no email from sign-up — nothing to verify.
+ if (!session.data && !search.email) {
+ throw redirect({ to: "/login" });
+ }
+
+ // Already verified — send them to the app.
+ if (session.data?.user.emailVerified) {
+ throw redirect({ to: "/" });
+ }
+ },
+});
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index f6aabeedf7..f6e5429dca 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -1,7 +1,11 @@
///
declare const __APP_BUILD_ID__: string;
-declare const __APP_TYPE__: "engine" | "inspector" | "cloud";
+
+interface ImportMetaEnv {
+ readonly VITE_FEATURE_FLAGS?: string;
+}
+
declare const Plain: {
// This takes the same arguments as Plain.init. It will update the chat widget in-place with the new configuration.
// Only top-level fields are updated, nested fields are not merged.
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index b698387d34..a4135a49b0 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -18,7 +18,6 @@
"include": [
"src",
"./vite-env.d.ts",
- "./vite.inspector.config.ts",
- "./vite.engine.config.ts"
+ "./vite.config.ts"
]
}
diff --git a/frontend/turbo.json b/frontend/turbo.json
index 22755f5fc6..b1a526b557 100644
--- a/frontend/turbo.json
+++ b/frontend/turbo.json
@@ -3,27 +3,13 @@
"extends": ["//"],
"tasks": {
"build": {
- "dependsOn": ["build:inspector"],
- "outputs": ["dist/**"]
- },
- "build:engine": {
- "dependsOn": ["^build"],
- "env": ["VITE_APP_*", "DEPLOYMENT_TYPE"],
- "outputs": ["dist/**"]
- },
- "build:inspector": {
- "dependsOn": ["^build"],
- "env": ["VITE_APP_*", "DEPLOYMENT_TYPE"],
- "outputs": ["dist/**"]
- },
- "build:cloud": {
"dependsOn": ["^build"],
- "env": ["VITE_APP_*", "DEPLOYMENT_TYPE"],
+ "env": ["VITE_APP_*", "VITE_FEATURE_FLAGS", "DEPLOYMENT_TYPE"],
"outputs": ["dist/**"]
},
"dev": {
"dependsOn": ["^build"],
- "env": ["VITE_APP_*"],
+ "env": ["VITE_APP_*", "VITE_FEATURE_FLAGS"],
"persistent": true
}
}
diff --git a/frontend/vite.base.config.ts b/frontend/vite.base.config.ts
index 01af10851f..26f2b53c10 100644
--- a/frontend/vite.base.config.ts
+++ b/frontend/vite.base.config.ts
@@ -8,7 +8,6 @@ export function baseViteConfig(): UserConfig {
return {
plugins: [tsconfigPaths()],
define: {
- __APP_TYPE__: JSON.stringify(process.env.APP_TYPE || "engine"),
__APP_BUILD_ID__: JSON.stringify(
`${new Date().toISOString()}@${crypto.randomUUID()}`,
),
diff --git a/frontend/vite.cloud.config.ts b/frontend/vite.cloud.config.ts
deleted file mode 100644
index a2ba6630af..0000000000
--- a/frontend/vite.cloud.config.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { defineConfig, loadEnv, mergeConfig } from "vite";
-import { cloudEnvSchema } from "./src/lib/env";
-import engineConfig, { liveChatPlugin } from "./vite.engine.config";
-
-// https://vitejs.dev/config/
-export default defineConfig((config) => {
- const env = loadEnv(config.mode, process.cwd(), "");
- cloudEnvSchema.parse(env);
- return mergeConfig(
- engineConfig(config),
- defineConfig({
- base: "/",
- plugins: [
- {
- ...liveChatPlugin(``),
- enforce: "pre",
- },
- ],
- define: {
- __APP_TYPE__: JSON.stringify("cloud"),
- },
- server: {
- port: 43710,
- },
- preview: {
- port: 43710,
- },
- }),
- true,
- );
-});
diff --git a/frontend/vite.engine.config.ts b/frontend/vite.config.ts
similarity index 70%
rename from frontend/vite.engine.config.ts
rename to frontend/vite.config.ts
index ebb3ef8d0f..d25f2f2654 100644
--- a/frontend/vite.engine.config.ts
+++ b/frontend/vite.config.ts
@@ -27,9 +27,17 @@ const getVariantForMode = (mode: string) => {
}
};
+function isFlagEnabled(featureFlags: string | undefined, flag: string): boolean {
+ if (featureFlags === undefined) return true;
+ return featureFlags.split(",").map((s) => s.trim()).includes(flag);
+}
+
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = commonEnvSchema.parse(loadEnv(mode, process.cwd(), ""));
+ const featureFlags = process.env.VITE_FEATURE_FLAGS;
+ const supportEnabled = isFlagEnabled(featureFlags, "support");
+ const multitenancyEnabled = isFlagEnabled(featureFlags, "multitenancy");
console.log(
env.SENTRY_AUTH_TOKEN
@@ -38,11 +46,31 @@ export default defineConfig(({ mode }) => {
);
return mergeConfig(baseViteConfig(), {
- base: "/ui",
+ base: multitenancyEnabled ? "/" : "/ui",
plugins: [
tanstackRouter({ target: "react", autoCodeSplitting: true }),
react(),
- liveChatPlugin(),
+ liveChatPlugin(
+ supportEnabled
+ ? ``
+ : "",
+ ),
env.SENTRY_AUTH_TOKEN
? sentryVitePlugin({
org: "rivet-gaming",
@@ -77,9 +105,6 @@ export default defineConfig(({ mode }) => {
preview: {
port: 43708,
},
- define: {
- __APP_TYPE__: JSON.stringify(env.APP_TYPE || "engine"),
- },
build: {
sourcemap: true,
commonjsOptions: {
diff --git a/package.json b/package.json
index 4f02dc9c00..a6a3129c81 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,7 @@
"@rivetkit/sqlite-vfs": "workspace:*",
"@rivetkit/engine-api-full": "workspace:*",
"@types/react": "^19",
- "@types/react-dom": "^19",
- "@clerk/shared": "3.27.1"
+ "@types/react-dom": "^19"
},
"pnpm": {
"overrides": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c467d84755..a20bda8772 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3246,6 +3246,9 @@ importers:
'@ladle/react':
specifier: ^5.1.1
version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ '@marsidev/react-turnstile':
+ specifier: ^1.5.0
+ version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
@@ -3501,6 +3504,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
+ es-toolkit:
+ specifier: ^1.45.1
+ version: 1.45.1
esast-util-from-js:
specifier: ^2.0.1
version: 2.0.1
@@ -7902,6 +7908,12 @@ packages:
resolution: {integrity: sha512-ZAK5gxYhGmfJqMjfWcRBjB8glITltDbTrYJXvcDtfengbKTZN0p39p5uO5pvUB8/PiAWKTRS06yaNMhf/LG26g==}
engines: {node: '>=20.0.0'}
+ '@marsidev/react-turnstile@1.5.0':
+ resolution: {integrity: sha512-Ph6mcj8u9WBDsBO7s9jKPsyRDz1sBPBJwrk+Ngx09vFInvKsQ6U6kW5amEcGq4dHOreB6DgFrOJk7/fy318YlQ==}
+ peerDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0
+
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
@@ -13246,6 +13258,9 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.45.1:
+ resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
+
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -23020,6 +23035,11 @@ snapshots:
optionalDependencies:
koffi: 2.15.4
+ '@marsidev/react-turnstile@1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
'@mdx-js/mdx@3.1.1':
dependencies:
'@types/estree': 1.0.8
@@ -29755,6 +29775,8 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
+ es-toolkit@1.45.1: {}
+
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 34265d5adf..2f47167e8f 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -24,7 +24,6 @@ packages:
- website/scripts/typecheck-staging
ignoredBuiltDependencies:
- - '@clerk/shared'
- '@parcel/watcher'
- '@sentry/cli'
- browser-tabs-lock
diff --git a/turbo.json b/turbo.json
index e0fad392e9..fbf44c0e3a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -15,26 +15,6 @@
"outputs": ["dist/**"],
"env": ["FAST_BUILD"]
},
- "build:cloud": {
- "dependsOn": ["^build"],
- "inputs": [
- "src/**",
- "tsconfig.json",
- "vite.cloud.config.ts",
- "package.json"
- ],
- "outputs": ["dist/**"]
- },
- "build:inspector": {
- "dependsOn": ["^build"],
- "inputs": [
- "src/**",
- "tsconfig.json",
- "vite.inspector.config.ts",
- "package.json"
- ],
- "outputs": ["dist/**"]
- },
"build:ladle": {
"dependsOn": ["^build"],
"inputs": [