-
- Failed to load latency data
+
+
+
+ Requests ({period})
+
+
+
+ {totalRequests.toLocaleString()}
- ) : (
- {[
- { label: "P50 Latency", value: l?.p50 ?? 0, icon: TrendingUp },
- { label: "P95 Latency", value: l?.p95 ?? 0, icon: TrendingUp },
- { label: "P99 Latency", value: l?.p99 ?? 0, icon: TrendingUp },
- { label: "Avg Latency", value: l?.avg ?? 0, icon: BarChart2 },
- ].map(({ label, value, icon: Icon }) => (
-
-
-
-
- {label}
-
-
-
- {value}ms
-
-
- ))}
+
+
+
+ Errors ({period})
+
+
+
0 ? "var(--color-danger)" : "var(--color-text-primary)",
+ }}
+ >
+ {totalErrors.toLocaleString()}
+
- )}
-
- {/* Request Trends */}
- {timeseriesLoading ? (
-
- ) : timeseriesError ? (
-
Failed to load request trends
+
+
+
+ Error Rate
+
+
+
1 ? "var(--color-danger)" : "var(--color-text-primary)",
+ }}
+ >
+ {errorRate}%
+
- ) : (
-
- Request Trends — {period}
-
-
-
-
- new Date(v).toLocaleTimeString([], { hour: "2-digit" })}
- stroke="var(--color-text-muted)"
- fontSize={11}
- />
-
-
-
-
-
-
+
+
+
+ P95 Latency
+
+
+
+ {l?.p95 ?? 0}ms
- )}
+
+
+ {/* Charts row */}
+
+ {/* Request Trends */}
+ {timeseriesLoading ? (
+
+ ) : timeseriesError ? (
+
+
Failed to load request trends
+
+ ) : (
+
+
+ Request Trends — {period}
+
+
+
+
+
+ new Date(v).toLocaleTimeString([], { hour: "2-digit" })
+ }
+ stroke="var(--color-text-muted)"
+ fontSize={11}
+ />
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Latency Distribution */}
+ {latencyLoading ? (
+
+ ) : latencyError ? (
+
+
Failed to load latency data
+
+ ) : (
+
+
+ Latency Distribution — {period}
+
+
+
+
+
+
+ [`${value}ms`, "Latency"]}
+ />
+
+ {latencyBars.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+ )}
+
{/* Top Endpoints Performance */}
{topEndpointsLoading ? (
@@ -292,7 +404,10 @@ export default function MetricsPage() {
className="border-t"
style={{ borderColor: "var(--color-border)" }}
>
-
+ |
#{i + 1}
|
diff --git a/apps/dashboard/src/pages/OverviewPage.tsx b/apps/dashboard/src/pages/OverviewPage.tsx
index 08e1540..d7c4f90 100644
--- a/apps/dashboard/src/pages/OverviewPage.tsx
+++ b/apps/dashboard/src/pages/OverviewPage.tsx
@@ -183,8 +183,13 @@ export default function OverviewPage() {
Recent Activity
-
- {(auditData?.logs ?? []).map((log: any) => (
+
+ {(auditData?.logs ?? []).length === 0 ? (
+
+ No recent activity yet.
+
+ ) : (
+ (auditData?.logs ?? []).map((log: any) => (
+ ))
+ )}
+
diff --git a/apps/dashboard/src/pages/StorageBucketPage.tsx b/apps/dashboard/src/pages/StorageBucketPage.tsx
index 0330f4e..dfbadab 100644
--- a/apps/dashboard/src/pages/StorageBucketPage.tsx
+++ b/apps/dashboard/src/pages/StorageBucketPage.tsx
@@ -24,13 +24,20 @@ export default function StorageBucketPage() {
const { data, isLoading } = useQuery({
queryKey: ["storageObjects", bucketName],
- queryFn: () => api.get(`/admin/storage/${bucketName}`),
+ queryFn: () => api.get<{ objects: { Key: string; Size: number; LastModified?: string; ETag?: string }[] }>(`/admin/storage/buckets/${bucketName}/objects`),
});
if (isLoading) return ;
const objects = data?.objects ?? [];
+ const formatSize = (bytes?: number) => {
+ if (bytes == null) return "-";
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
return (
Name
Size
- Type
- Created
+ Last Modified
Actions
{objects
- .filter((o: any) => o.name.includes(search))
- .map((obj: any) => (
-
- {obj.name}
-
- {obj.size}
-
+ .filter((o) => o.Key.includes(search))
+ .map((obj) => (
+
+ {obj.Key}
- {obj.content_type}
+ {formatSize(obj.Size)}
- {obj.created_at ? new Date(obj.created_at).toLocaleDateString() : "-"}
+ {obj.LastModified
+ ? new Date(obj.LastModified).toLocaleDateString()
+ : "-"}
@@ -106,10 +111,10 @@ export default function StorageBucketPage() {
))}
-
-
-
- )}
+
+
+
+ )}
);
diff --git a/apps/dashboard/src/pages/StoragePage.tsx b/apps/dashboard/src/pages/StoragePage.tsx
index 0096e06..ab2b2f6 100644
--- a/apps/dashboard/src/pages/StoragePage.tsx
+++ b/apps/dashboard/src/pages/StoragePage.tsx
@@ -1,18 +1,46 @@
import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader";
import { PageSkeleton } from "@/components/ui/PageSkeleton";
+import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import { api } from "@/lib/api";
import { QK } from "@/lib/query-keys";
-import { formatDate } from "@/lib/utils";
-import { useQuery } from "@tanstack/react-query";
-import { FolderOpen, HardDrive } from "lucide-react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { FolderOpen, HardDrive, Loader2, Plus } from "lucide-react";
+import { useState } from "react";
import { Link } from "react-router";
+import { toast } from "sonner";
export default function StoragePage() {
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [bucketName, setBucketName] = useState("");
+
const { data, isLoading } = useQuery({
queryKey: QK.storageBuckets(),
- queryFn: () => api.get("/admin/storage"),
+ queryFn: () => api.get<{ buckets: { Name: string; CreationDate?: string }[] }>("/admin/storage/buckets"),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (name: string) => api.post("/admin/storage/buckets", { name }),
+ onSuccess: () => {
+ toast.success("Bucket created");
+ setOpen(false);
+ setBucketName("");
+ queryClient.invalidateQueries({ queryKey: QK.storageBuckets() });
+ },
+ onError: (err: any) => toast.error(err.message),
});
if (isLoading) return ;
@@ -21,19 +49,70 @@ export default function StoragePage() {
return (
-
+
+
+
+
+
+
+
+ Create Bucket
+
+
+ Create a new storage bucket (pocket) for your projects.
+
+
+
+
+
+ setBucketName(e.target.value)}
+ required
+ />
+
+
+
+
+
+
+
+
+ }
+ />
{buckets.length === 0 ? (
) : (
- {buckets.map((bucket: any) => (
-
+ {buckets.map((bucket) => (
+
@@ -44,11 +123,16 @@ export default function StoragePage() {
-
- {bucket.name}
+
+ {bucket.Name}
- {bucket.public ? "Public" : "Private"}
+ {bucket.CreationDate
+ ? new Date(bucket.CreationDate).toLocaleDateString()
+ : "S3 Bucket"}
diff --git a/apps/dashboard/src/pages/TeamPage.tsx b/apps/dashboard/src/pages/TeamPage.tsx
index f4a7758..756efa3 100644
--- a/apps/dashboard/src/pages/TeamPage.tsx
+++ b/apps/dashboard/src/pages/TeamPage.tsx
@@ -478,7 +478,10 @@ export default function TeamPage() {
assignRoleMutation.mutate({
admin_id: formData.get("admin_id") as string,
role_id: formData.get("role_id") as string,
- project_id: (formData.get("project_id") as string) || undefined,
+ project_id:
+ (formData.get("project_id") as string) === "__global__"
+ ? undefined
+ : (formData.get("project_id") as string) || undefined,
});
}}
>
@@ -520,7 +523,7 @@ export default function TeamPage() {
- Global (no scope)
+ Global (no scope)
{projectsData?.projects?.map((p) => (
{p.name}
diff --git a/apps/dashboard/src/pages/projects/ProjectLayout.tsx b/apps/dashboard/src/pages/projects/ProjectLayout.tsx
index 66c5969..1b98cb2 100644
--- a/apps/dashboard/src/pages/projects/ProjectLayout.tsx
+++ b/apps/dashboard/src/pages/projects/ProjectLayout.tsx
@@ -60,7 +60,9 @@ export default function ProjectLayout() {
];
const currentPath = location.pathname;
- const activeTab = tabs.find((tab) => currentPath.startsWith(tab.href))?.value ?? "overview";
+ const activeTab =
+ [...tabs].sort((a, b) => b.href.length - a.href.length).find((tab) => currentPath.startsWith(tab.href))
+ ?.value ?? "overview";
return (
diff --git a/apps/dashboard/src/pages/settings/InngestDashboardPage.tsx b/apps/dashboard/src/pages/settings/InngestDashboardPage.tsx
index c72250b..fc76343 100644
--- a/apps/dashboard/src/pages/settings/InngestDashboardPage.tsx
+++ b/apps/dashboard/src/pages/settings/InngestDashboardPage.tsx
@@ -2,6 +2,8 @@ import { PageHeader } from "@/components/ui/PageHeader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
@@ -10,6 +12,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { api } from "@/lib/api";
import {
type InngestFunction,
type InngestRun,
@@ -24,6 +27,8 @@ import {
Clock,
Loader2,
PlayCircle,
+ Save,
+ Settings,
XCircle,
} from "lucide-react";
import { useState } from "react";
@@ -62,6 +67,12 @@ export default function InngestDashboardPage() {
const queryClient = useQueryClient();
const [selectedFunction, setSelectedFunction] = useState (null);
const [runStatusFilter, setRunStatusFilter] = useState("");
+ const [showConfig, setShowConfig] = useState(false);
+ const [configForm, setConfigForm] = useState({
+ inngest_api_key: "",
+ inngest_env_id: "",
+ inngest_base_url: "",
+ });
// Connection status
const {
@@ -108,6 +119,23 @@ export default function InngestDashboardPage() {
onError: (err: any) => toast.error(err.message ?? "Failed to cancel run"),
});
+ // Save config mutation
+ const saveConfigMutation = useMutation({
+ mutationFn: (data: {
+ inngest_api_key?: string;
+ inngest_env_id?: string;
+ inngest_base_url?: string;
+ }) => api.patch("/admin/instance", data),
+ onSuccess: () => {
+ toast.success("Inngest configuration saved");
+ setShowConfig(false);
+ refetchStatus();
+ },
+ onError: (err: any) => toast.error(err.message ?? "Failed to save configuration"),
+ });
+
+ const isConnected = status?.status === "connected";
+
return (
@@ -126,7 +154,7 @@ export default function InngestDashboardPage() {
{statusLoading ? (
- ) : status?.status === "connected" ? (
+ ) : isConnected ? (
@@ -134,14 +162,93 @@ export default function InngestDashboardPage() {
) : (
-
-
-
- {status?.error ?? "Unable to connect to Inngest"}
-
-
+
+
+
+
+ {status?.error ?? "Unable to connect to Inngest"}
+
+
+
+ {!showConfig && (
+
+ )}
+ {showConfig && (
+
+
+
+
+ setConfigForm((f) => ({ ...f, inngest_api_key: e.target.value }))
+ }
+ />
+
+
+
+
+ setConfigForm((f) => ({ ...f, inngest_env_id: e.target.value }))
+ }
+ />
+
+
+
+
+ setConfigForm((f) => ({ ...f, inngest_base_url: e.target.value }))
+ }
+ />
+
+ Leave empty for Inngest Cloud. Use http://localhost:8288 for local dev
+ server.
+
+
+
+
+
+
+
+ )}
)}
diff --git a/bun.lock b/bun.lock
index d0a30e5..f7b0899 100644
--- a/bun.lock
+++ b/bun.lock
@@ -59,6 +59,15 @@
"vite": "^6.0.6",
},
},
+ "apps/test-project": {
+ "name": "test-project",
+ "version": "0.1.0",
+ "dependencies": {
+ "@betterbase/client": "workspace:*",
+ "@betterbase/core": "workspace:*",
+ "hono": "^4.0.0",
+ },
+ },
"packages/cli": {
"name": "@betterbase/cli",
"version": "0.1.0",
@@ -574,7 +583,7 @@
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
- "@neondatabase/serverless": ["@neondatabase/serverless@1.0.2", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw=="],
+ "@neondatabase/serverless": ["@neondatabase/serverless@1.1.0", "", {}, "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q=="],
"@next/env": ["@next/env@15.5.12", "", {}, "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg=="],
@@ -1114,7 +1123,7 @@
"@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
- "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/nodemailer": ["@types/nodemailer@6.4.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-aFV3/NsYFLSx9mbb5gtirBSXJnAlrusoKNuPbxsASWc7vrKLmIrTQRpdcxNcSFL3VW2A2XpeLEavwb2qMi6nlQ=="],
@@ -1670,6 +1679,8 @@
"temporal-spec": ["temporal-spec@0.2.4", "", {}, "sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ=="],
+ "test-project": ["test-project@workspace:apps/test-project"],
+
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
@@ -1702,7 +1713,7 @@
"ulid": ["ulid@2.4.0", "", { "bin": { "ulid": "bin/cli.js" } }, "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg=="],
- "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
@@ -1768,7 +1779,13 @@
"@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
- "@betterbase/core/@libsql/client": ["@libsql/client@0.17.2", "", { "dependencies": { "@libsql/core": "^0.17.2", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q=="],
+ "@betterbase/core/@libsql/client": ["@libsql/client@0.17.3", "", { "dependencies": { "@libsql/core": "^0.17.3", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-HXk9wiAoJbKFbyBH4O+aEhN6ir5ERXuXvwE5OD2eR4/5RUa3Pw/8L9zrnVdU+iNJitRvisPWaIwmhkO3bH7giA=="],
+
+ "@betterbase/core/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
+
+ "@betterbase/core/postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
+
+ "@betterbase/shared/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
@@ -1778,8 +1795,12 @@
"@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="],
+ "@inngest/ai/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
+
"@inquirer/core/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="],
+ "@inquirer/core/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
+
"@inquirer/core/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
@@ -1824,26 +1845,6 @@
"@traceloop/instrumentation-anthropic/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ=="],
- "@types/bunyan/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/connect/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/memcached/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/mute-stream/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/mysql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/nodemailer/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/oracledb/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/pg/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "@types/ws/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
"better-auth/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@@ -1852,8 +1853,6 @@
"betterbase-base-template/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
- "bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -1882,8 +1881,6 @@
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
- "protobufjs/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -1894,10 +1891,16 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
- "@betterbase/core/@libsql/client/@libsql/core": ["@libsql/core@0.17.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g=="],
+ "@betterbase/core/@libsql/client/@libsql/core": ["@libsql/core@0.17.3", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-2UjK1i7JBkMduJo4WdvvBxMMvVJ31pArBZNONyz/GCJJAH+1UHat2X6vn10S/WpY5fKzIT98WqYFl2vzWRLOfg=="],
+
+ "@betterbase/core/@libsql/client/@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="],
"@betterbase/core/@libsql/client/libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
+ "@betterbase/core/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
+
+ "@betterbase/shared/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
@@ -1942,9 +1945,11 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
- "@inquirer/core/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "@inngest/ai/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
- "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+ "@inquirer/core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "@inquirer/core/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@opentelemetry/instrumentation-pg/@types/pg/pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
@@ -1954,30 +1959,8 @@
"@traceloop/instrumentation-anthropic/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
- "@types/bunyan/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/connect/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/memcached/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/mute-stream/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/mysql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/nodemailer/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/oracledb/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
- "@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
"betterbase-base-template/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
- "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"inngest/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
@@ -2024,8 +2007,6 @@
"ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
- "protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -2056,12 +2037,6 @@
"@betterbase/core/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
- "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
-
"@traceloop/instrumentation-anthropic/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
-
- "betterbase-base-template/@types/bun/bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
-
- "betterbase-base-template/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}
diff --git a/check-admin.ts b/check-admin.ts
new file mode 100644
index 0000000..fd069cd
--- /dev/null
+++ b/check-admin.ts
@@ -0,0 +1,10 @@
+import { Client } from 'pg'
+
+const client = new Client({
+ connectionString: 'postgresql://neondb_owner:npg_NDrg3StRE4jY@ep-still-thunder-an4tpncc-pooler.c-6.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require'
+})
+
+await client.connect()
+const res = await client.query("SELECT id, email FROM betterbase_meta.admin_users")
+console.log(JSON.stringify(res.rows, null, 2))
+await client.end()
diff --git a/check-hash.ts b/check-hash.ts
new file mode 100644
index 0000000..4e4b269
--- /dev/null
+++ b/check-hash.ts
@@ -0,0 +1,10 @@
+import { Client } from 'pg'
+
+const client = new Client({
+ connectionString: 'postgresql://neondb_owner:npg_NDrg3StRE4jY@ep-still-thunder-an4tpncc-pooler.c-6.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require'
+})
+
+await client.connect()
+const res = await client.query("SELECT password_hash FROM betterbase_meta.admin_users WHERE email = 'admin@example.com'")
+console.log('Stored hash:', res.rows[0]?.password_hash)
+await client.end()
diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts
index b1b845c..33f832a 100644
--- a/packages/cli/src/commands/login.ts
+++ b/packages/cli/src/commands/login.ts
@@ -68,12 +68,27 @@ export async function runLoginCommand(opts: { serverUrl?: string } = {}) {
process.exit(1);
}
+ const fullVerificationUri = `${verificationUri}?code=${userCode}`;
+
keyValue("Instance", serverUrl);
keyValue("Your code", chalk.bold(chalk.yellow(userCode)));
blank();
- console.log(` ${chalk.dim("Open:")} ${chalk.cyan(`${verificationUri}?code=${userCode}`)}`);
+ console.log(` ${chalk.dim("Open:")} ${chalk.cyan(fullVerificationUri)}`);
blank();
- console.log(chalk.dim(" Waiting for browser authorization") + chalk.dim(" (5 min timeout)..."));
+
+ // Try to open the browser automatically
+ try {
+ if (process.platform === "darwin") {
+ await Bun.spawn(["open", fullVerificationUri]);
+ } else if (process.platform === "win32") {
+ await Bun.spawn(["cmd", "/c", "start", fullVerificationUri]);
+ } else {
+ await Bun.spawn(["xdg-open", fullVerificationUri]);
+ }
+ console.log(chalk.dim(" Browser opened. Waiting for authorization..."));
+ } catch {
+ console.log(chalk.dim(" Waiting for browser authorization") + chalk.dim(" (5 min timeout)..."));
+ }
// Step 2: Poll for token
const deadline = Date.now() + POLL_TIMEOUT_MS;
diff --git a/packages/server/src/routes/admin/inngest.ts b/packages/server/src/routes/admin/inngest.ts
index aaf4e4e..56be886 100644
--- a/packages/server/src/routes/admin/inngest.ts
+++ b/packages/server/src/routes/admin/inngest.ts
@@ -3,8 +3,17 @@ import { getPool } from "../../lib/db";
export const inngestAdminRoutes = new Hono();
-const getInngestBaseUrl = (): string => {
- return process.env.INNGEST_BASE_URL ?? "https://api.inngest.com";
+const getInngestBaseUrl = async (): Promise => {
+ const envUrl = process.env.INNGEST_BASE_URL;
+ if (envUrl) return envUrl;
+
+ const pool = getPool();
+ const { rows } = await pool.query(
+ "SELECT value FROM betterbase_meta.instance_settings WHERE key = 'inngest_base_url'",
+ );
+ const storedValue = rows[0]?.value;
+ const url = typeof storedValue === "string" ? storedValue : (storedValue?.value ?? null);
+ return url || "https://api.inngest.com";
};
const getInngestHeaders = async (): Promise => {
@@ -33,8 +42,8 @@ const getInngestEnv = async (): Promise => {
return typeof storedValue === "string" ? storedValue : (storedValue?.value ?? null);
};
-const isSelfHosted = (): boolean => {
- const baseUrl = getInngestBaseUrl();
+const isSelfHosted = async (): Promise => {
+ const baseUrl = await getInngestBaseUrl();
return baseUrl !== "https://api.inngest.com";
};
@@ -51,9 +60,9 @@ const fetchWithErrorCheck = async (url: string, options?: RequestInit) => {
// GET /admin/inngest/status — Check Inngest connection status
inngestAdminRoutes.get("/status", async (c) => {
try {
- const baseUrl = getInngestBaseUrl();
+ const baseUrl = await getInngestBaseUrl();
- if (isSelfHosted()) {
+ if (await isSelfHosted()) {
const res = await fetch(`${baseUrl}/health`);
const healthy = res.ok;
@@ -84,11 +93,11 @@ inngestAdminRoutes.get("/status", async (c) => {
// GET /admin/inngest/functions — List all registered functions
inngestAdminRoutes.get("/functions", async (c) => {
try {
- const baseUrl = getInngestBaseUrl();
+ const baseUrl = await getInngestBaseUrl();
const headers = await getInngestHeaders();
const envId = await getInngestEnv();
- if (isSelfHosted()) {
+ if (await isSelfHosted()) {
// Self-hosted Inngest has different API structure
// Return local functions from inngest.ts
const { allInngestFunctions } = await import("../../lib/inngest");
@@ -121,7 +130,7 @@ inngestAdminRoutes.get("/functions", async (c) => {
inngestAdminRoutes.get("/functions/:id/runs", async (c) => {
try {
const functionId = c.req.param("id");
- const baseUrl = getInngestBaseUrl();
+ const baseUrl = await getInngestBaseUrl();
const headers = await getInngestHeaders();
const envId = await getInngestEnv();
@@ -131,7 +140,7 @@ inngestAdminRoutes.get("/functions/:id/runs", async (c) => {
const params = new URLSearchParams({ limit: String(limit) });
if (status) params.append("status", status);
- if (isSelfHosted()) {
+ if (await isSelfHosted()) {
// Self-hosted: query from database webhook_deliveries by webhook_id
// Note: functionId in routes refers to webhook ID for webhook deliveries
const pool = getPool();
@@ -174,11 +183,11 @@ inngestAdminRoutes.get("/functions/:id/runs", async (c) => {
inngestAdminRoutes.get("/runs/:runId", async (c) => {
try {
const runId = c.req.param("runId");
- const baseUrl = getInngestBaseUrl();
+ const baseUrl = await getInngestBaseUrl();
const headers = await getInngestHeaders();
const envId = await getInngestEnv();
- if (isSelfHosted()) {
+ if (await isSelfHosted()) {
// Self-hosted: get from database
const pool = getPool();
const { rows } = await pool.query(
@@ -306,11 +315,11 @@ inngestAdminRoutes.post("/functions/:id/test", async (c) => {
inngestAdminRoutes.post("/runs/:runId/cancel", async (c) => {
try {
const runId = c.req.param("runId");
- const baseUrl = getInngestBaseUrl();
+ const baseUrl = await getInngestBaseUrl();
const headers = await getInngestHeaders();
const envId = await getInngestEnv();
- if (isSelfHosted()) {
+ if (await isSelfHosted()) {
// Self-hosted: cannot cancel (webhooks are synchronous from DB perspective)
return c.json(
{
diff --git a/packages/server/src/routes/admin/instance.ts b/packages/server/src/routes/admin/instance.ts
index 9468cf4..58b891c 100644
--- a/packages/server/src/routes/admin/instance.ts
+++ b/packages/server/src/routes/admin/instance.ts
@@ -31,6 +31,9 @@ instanceRoutes.patch(
require_email_verification: z.boolean().optional(),
ip_allowlist: z.array(z.string()).optional(),
cors_origins: z.array(z.string().url()).optional(),
+ inngest_api_key: z.string().optional(),
+ inngest_env_id: z.string().optional(),
+ inngest_base_url: z.string().url().optional().or(z.literal("")),
}),
),
async (c) => {
diff --git a/reset-password.ts b/reset-password.ts
new file mode 100644
index 0000000..54e9ce7
--- /dev/null
+++ b/reset-password.ts
@@ -0,0 +1,22 @@
+import { Client } from 'pg'
+import bcrypt from 'bcryptjs'
+
+const client = new Client({
+ connectionString: 'postgresql://neondb_owner:npg_NDrg3StRE4jY@ep-still-thunder-an4tpncc-pooler.c-6.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require'
+})
+
+await client.connect()
+
+const newPassword = 'AdminPass123!'
+const hash = await bcrypt.hash(newPassword, 12)
+
+await client.query(`
+ UPDATE betterbase_meta.admin_users
+ SET password_hash = $1
+ WHERE email = 'admin@example.com'
+`, [hash])
+
+console.log('Password updated to:', newPassword)
+console.log('Hash:', hash)
+
+await client.end()
|