If you don't want to use the Keyset hosted dashboard — or you want to embed content management directly into your own admin interface — you can build a fully self-hosted CMS panel using only the SDK and a secret key.
skey_…)keysetapp packageThe SDK exposes four methods that together give you everything a CMS needs:
| Method | What it does |
|---|---|
content.dashboardList(projectId) | Fetch all entries with full metadata (id, createdAt, updatedAt) |
content.create(entry) | Create a new entry |
content.update(key, entry) | Edit an existing entry's value and/or type |
content.delete(key) | Remove an entry |
TypeScript JSXimport { useEffect, useState, useCallback } from "react"; import { Keyset } from "keysetapp"; import type { DashboardContentEntry } from "keysetapp"; const client = new Keyset({ apiKey: "skey_…", cacheTTL: 0 }); const PROJECT_ID = "your_project_id"; export function MyCMSPanel() { const [entries, setEntries] = useState<DashboardContentEntry[]>([]); const load = useCallback(async () => { const res = await client.content.dashboardList(PROJECT_ID); setEntries(res.data); }, []); useEffect(() => { load(); }, [load]); async function handleCreate() { await client.content.create({ key: "welcome_message", value: "Hello from my CMS!", type: "string", }); load(); // refresh the list } async function handleUpdate(key: string, newValue: string) { await client.content.update(key, { value: newValue, type: "string" }); load(); } async function handleDelete(key: string) { await client.content.delete(key); load(); } return ( <div> <button onClick={handleCreate}>Create entry</button> <ul> {entries.map((entry) => ( <li key={entry.id}> <strong>{entry.key}</strong>: {String(entry.value)} <button onClick={() => handleUpdate(entry.key, "New value")}>Edit</button> <button onClick={() => handleDelete(entry.key)}>Delete</button> </li> ))} </ul> </div> ); }
DashboardContentEntry shapedashboardList returns entries with more metadata than the regular read methods:
TypeScriptinterface DashboardContentEntry { id: string; // server-generated entry ID key: string; // your config key value: unknown; // the stored value type: ContentType; // "string" | "number" | "boolean" | "json" | "url" | "image" createdAt: string; // ISO 8601 updatedAt: string; // ISO 8601 }
This is enough to power a full CRUD admin interface.
A complete, styled CMS panel is included in the demo project at:
/demo/src/demos/CMSPanel.tsx
It demonstrates:
Run the demo with:
Shellcd demo && npm install && npm run dev
Then open the Custom CMS Panel tab.
pkey_…) throws "This operation requires a secret key" before making any network call — the SDK enforces this automatically.Browser admin UI
│
│ (calls your own API)
▼
Your backend (Node.js / Edge)
│ holds skey_… securely
│
▼
keysetapp ──► Keyset API
This keeps the secret key off the client entirely while still giving you a fully custom management UI.