diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 548c3190..dc291362 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -138,7 +138,7 @@ Main flow modules:
Primary state DB:
- `src/lib/localDb.js`
-- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset)
+- file: `${DATA_DIR}/db/data.sqlite` (or `~/.9router/db/data.sqlite` when `DATA_DIR` is unset)
- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing
Usage DB:
@@ -151,7 +151,8 @@ Usage DB:
- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`
- API key generation/verification: `src/shared/utils/apiKey.js`
-- Provider secrets persisted in `providerConnections` entries
+- Provider secrets persisted in `providerConnections` entries. Provider management is administrator-only: regular users cannot access the Providers dashboard or provider/OAuth management APIs, but their API keys route through the shared active administrator credential pool.
+- Schema migration 007 deletes legacy provider connections owned by regular users or missing users, and assigns legacy ownerless connections to the first administrator.
- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`)
## 5) Cloud Sync
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js
index b87a1ade..845eed85 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js
@@ -10,7 +10,6 @@ import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { getThinkingLevels } from "open-sse/providers/thinkingLevels.js";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { useModelCaps } from "@/shared/hooks/useModelCaps";
-import useUserStore from "@/store/userStore";
import { translate } from "@/i18n/runtime";
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
import { getProviderCustomModelRows } from "@/shared/utils/providerCustomModels";
@@ -39,7 +38,6 @@ export default function ProviderDetailPage() {
const router = useRouter();
const providerId = params.id;
const { getCaps } = useModelCaps();
- const user = useUserStore((state) => state.user);
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
const [providerNode, setProviderNode] = useState(null);
@@ -81,8 +79,6 @@ export default function ProviderDetailPage() {
const [importingQoderModels, setImportingQoderModels] = useState(false);
const { copied, copy } = useCopyToClipboard();
- const canManageModelAvailability = user?.role === "admin";
-
const AG_RISK_STORAGE_KEY = "ag_risk_confirmed";
const openOAuthConnection = () => {
@@ -1121,7 +1117,7 @@ export default function ProviderDetailPage() {
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
isTesting={testingModelIds.has(model.id)}
isFree={model.isFree}
- onDisable={canManageModelAvailability ? () => handleDisableModel(model.id) : undefined}
+ onDisable={() => handleDisableModel(model.id)}
caps={getCaps(`${providerId}/${model.id}`)}
thinkingSuffix={resolveThinkingSuffix(model.id)}
/>
@@ -1185,7 +1181,7 @@ export default function ProviderDetailPage() {
})()}
{/* Disabled models — restorable */}
- {canManageModelAvailability && disabledDisplayModels.length > 0 && (
+ {disabledDisplayModels.length > 0 && (
Disabled models ({disabledDisplayModels.length}):
@@ -1616,7 +1612,7 @@ export default function ProviderDetailPage() {
)}
- {canManageModelAvailability && !isCompatible && (() => {
+ {!isCompatible && (() => {
const allIds = [
...models,
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
diff --git a/src/app/(dashboard)/dashboard/providers/new/page.js b/src/app/(dashboard)/dashboard/providers/new/page.js
deleted file mode 100644
index 57d0a7f5..00000000
--- a/src/app/(dashboard)/dashboard/providers/new/page.js
+++ /dev/null
@@ -1,220 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import Link from "next/link";
-import { Card, Button, Input, Select, Toggle } from "@/shared/components";
-import { AI_PROVIDERS, AUTH_METHODS } from "@/shared/constants/config";
-
-const providerOptions = Object.values(AI_PROVIDERS).map((p) => ({
- value: p.id,
- label: p.name,
-}));
-
-const authMethodOptions = Object.values(AUTH_METHODS).map((m) => ({
- value: m.id,
- label: m.name,
-}));
-
-export default function NewProviderPage() {
- const router = useRouter();
- const [loading, setLoading] = useState(false);
- const [formData, setFormData] = useState({
- provider: "",
- authMethod: "api_key",
- apiKey: "",
- displayName: "",
- isActive: true,
- });
- const [errors, setErrors] = useState({});
-
- const handleChange = (field, value) => {
- setFormData((prev) => ({ ...prev, [field]: value }));
- if (errors[field]) {
- setErrors((prev) => ({ ...prev, [field]: null }));
- }
- };
-
- const validate = () => {
- const newErrors = {};
- if (!formData.provider) newErrors.provider = "Please select a provider";
- if (formData.authMethod === "api_key" && !formData.apiKey) {
- newErrors.apiKey = "API Key is required";
- }
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!validate()) return;
-
- setLoading(true);
- try {
- const response = await fetch("/api/providers", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(formData),
- });
-
- if (response.ok) {
- router.push("/dashboard/providers");
- } else {
- const data = await response.json();
- setErrors({ submit: data.error || "Failed to create provider" });
- }
- } catch (error) {
- setErrors({ submit: "An error occurred. Please try again." });
- } finally {
- setLoading(false);
- }
- };
-
- const selectedProvider = AI_PROVIDERS[formData.provider];
-
- return (
-
- {/* Header */}
-
-
-
arrow_back
- Back to Providers
-
-
Add New Provider
-
- Configure a new AI provider to use with your applications.
-
-
-
- {/* Form */}
-
-
-
-
- );
-}
-
diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js
index 6db6c9e8..7d52d30b 100644
--- a/src/app/(dashboard)/dashboard/providers/page.js
+++ b/src/app/(dashboard)/dashboard/providers/page.js
@@ -22,7 +22,6 @@ import Link from "next/link";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
import { useNotificationStore } from "@/store/notificationStore";
import { useHeaderSearchStore } from "@/store/headerSearchStore";
-import useUserStore from "@/store/userStore";
import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
import AddCompatibleModal from "./components/AddCompatibleModal";
@@ -106,12 +105,9 @@ export default function ProvidersPage() {
const [testingMode, setTestingMode] = useState(null);
const [testResults, setTestResults] = useState(null);
const notify = useNotificationStore();
- const user = useUserStore((state) => state.user);
const searchQuery = useHeaderSearchStore((s) => s.query);
const registerSearch = useHeaderSearchStore((s) => s.register);
const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
- const isAdmin = user?.role === "admin";
-
useEffect(() => {
registerSearch("Search providers...");
return () => unregisterSearch();
@@ -287,7 +283,6 @@ export default function ProvidersPage() {
.filter(
([, info]) =>
!info.hidden &&
- (user?.role === "admin" || !info.noAuth) &&
matchSearch(info.name),
)
.sort(([, a], [, b]) => (b.noAuth ? 1 : 0) - (a.noAuth ? 1 : 0));
@@ -295,7 +290,6 @@ export default function ProvidersPage() {
Object.entries(FREE_TIER_PROVIDERS).filter(
([, info]) =>
!info.hidden &&
- (user?.role === "admin" || !info.noAuth) &&
matchSearch(info.name) &&
(info.serviceKinds ?? ["llm"]).includes("llm"),
),
@@ -336,10 +330,10 @@ export default function ProvidersPage() {
freeEntries.length > 0 ||
freeTierEntries.length > 0 ||
apikeyEntries.length > 0 ||
- (isAdmin && (
+ (
compatibleProviders.length > 0 ||
anthropicCompatibleProviders.length > 0
- ));
+ );
return (
@@ -353,7 +347,6 @@ export default function ProvidersPage() {
)}
{/* Custom provider configuration is administered centrally. */}
- {isAdmin && (
@@ -404,7 +397,6 @@ export default function ProvidersPage() {
)}
- )}
{/* OAuth Providers */}
{oauthEntries.length > 0 && (
@@ -582,7 +574,7 @@ export default function ProvidersPage() {
*/}
- {isAdmin &&