Admin UI

Every Sluurp server has a dashboard at /_/, like Django admin: generated from your schema, zero code. Each collection gets a table to browse, filter and edit, plus screens to operate the server.

For your data

  • A table per collection with a filter box (paid = true && total > 100), sorting, column picker and paging.
  • A form per record with the right input per field: date pickers, searchable relation pickers, image preview and upload.
  • History and undo: every change with who and why, restore any version, deleted records in a recycle bin.
  • Explore and pivot tables, charts and formulas over a collection, no export needed.
  • Rules for read and write access, edited next to the collection.

For the server

Logs, SQL console, backups, scheduled jobs, rate limits, mail, sign-in providers, payments, forms, feeds, translations and storage, each on its own screen. Reorder the Platform items by dragging or with Alt+arrow keys. Ctrl+K (⌘K) jumps to any screen, collection or record.

Logs

Every request is logged: time, method, path, status, duration, user and IP. Search them with Kibana’s query language (KQL):

status:5xx and path:/api/*
method:POST or took>500
not who:_superusers* and (status:4xx or status:5xx)
time>=2026-09-20 and time<now-2h
timeout 10.0.0.7
  • field:value matches: status by class (2xx) or code, method exactly, path, who and ip by substring, or by wildcard with *.
  • =, !=, >, >=, <, <= compare. time accepts a date, a datetime, or now-15m, now-2h, now-7d.
  • Combine with and, or, not and parentheses. Adjacent terms are ANDed. A bare word matches anywhere.

The database keeps one week of logs. Older entries are archived to a gzipped file per day (logs/2026-09-18.jsonl.gz in the data directory), kept for 90 days. Searching a longer window transparently includes the archives.

API

The API screen lists every endpoint from the project’s OpenAPI document, grouped by collection. Choose one, fill in its parameters and body (pre-filled from the collection’s fields), and send it as yourself. You see the status, the time and the response, and can copy the request as curl or download openapi.json for a client generator.

UI kit

The UI kit screen is a component workshop, like Storybook. It has three modes:

  • Docs: a page per component, with every prop taken from its source and each example with the code that draws it.
  • Controls: one component with a field for each prop it takes.
  • Playground: edit the code and see it drawn as you type, at one width or at phone, tablet and desktop side by side.

Stories use Storybook’s Component Story Format (CSF 3), so story files work in both. Any *.stories.tsx next to a component is picked up:

client/kit/button.stories.tsx
import { Button } from "./button.js";

export default {
  title: "Actions/Button",
  component: Button,
  args: { children: "Save changes", disabled: false },
  argTypes: { variant: { control: "select", options: ["default", "outline", "destructive"] } },
};

export const Primary = {};
export const Destructive = { args: { variant: "destructive", children: "Delete" } };

Without render, a story draws component(args). Controls are inferred from the args’ values; argTypes is only needed for a select. play({ canvasElement, args }) runs after drawing. parameters.order places a story within its group when A to Z is not the order to read them in.

One Sluurp extension (Storybook ignores it): argTypes: { collection: { control: "collection" } } lets you pick a collection from the current project, and its rows are passed to render(args, { loaded: { rows } }). Every story is also drawn in a test.

Your own components

An app’s own story files show up too, under Custom, after the kit’s. So a separate app can be a library of your own components, including ones built on the kit’s:

components/gradient-button.tsx
import { Button, type ButtonProps } from "sluurp/kit/button.js";

export interface GradientButtonProps extends ButtonProps {
  /** Where the gradient starts and ends. */
  tone?: "sunset" | "ocean";
}

export function GradientButton({ tone = "sunset", className, ...rest }: GradientButtonProps = {}) {
  return Button({ ...rest, className: `bg-linear-to-r text-white ${tone === "sunset" ? "from-amber-400 to-rose-500" : "from-sky-400 to-indigo-600"} ${className ?? ""}` });
}
components/gradient-button.stories.tsx
import { GradientButton } from "./gradient-button.tsx";

export default {
  title: "Buttons/Gradient Button",
  component: GradientButton,
  args: { children: "Book the trip" },
};

export const Sunset = {};
export const Ocean = { args: { tone: "ocean" } };

To give it its own icon in the UI kit’s list, set parameters.icon to an SVG, as markup or an element. Without one, it gets the default grid icon:

export default {
  title: "Buttons/Gradient Button",
  component: GradientButton,
  parameters: {
    icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="10" x="3" y="7" rx="5"/></svg>',
  },
};

Use stroke="currentColor" (or fill) so the icon follows the list’s colours, light and dark, selected or not.

Its props table lists tone and everything it takes from ButtonProps. Its examples import it from its own file and open in the Playground like the kit’s. The app can be the one at / or one mounted beside it (--public ui=path/to/app).

Compared with Django admin

Sluurp has, Django admin doesn’t:

  • Live updates: records changed by others update on your screen.
  • Built-in history, restore and recycle bin for every collection.
  • SQL console, backups, logs, jobs and rate limits in one place.
  • Nothing to register: a collection appears as soon as it exists.
  • Collaborative live translation editing.

Django admin has, Sluurp doesn’t (yet):

  • Per-model admin customisation in code: visible columns, form grouping, read-only fields.
  • Custom bulk actions on selected rows.
  • Inline editing of related records in the parent form.
  • Date drill-down (year → month → day) above lists.

The admin UI is built with the same UI kit your apps use.