portable-admin
One admin for every app.
portable-admin is a single admin dashboard for all my small Postgres apps - Dönerbuch, Watchly, LapBattle and whatever comes next. It’s deployed once, and apps are connected in the UI by their database URL. It reads each app’s schema live, so a new app needs no code: every table can be browsed, searched, edited and deleted, and every change is logged and can be undone.
What it is
- Connect, don’t deploy. Add an app with its database URL, test the connection, and its tables show up. No per-app admin to build or host.
- Every table, generically. Search (accents folded, so “doner” finds “Döner”), filters, sorting, row pages with what references them, edit, delete, merge, notes and per-row history.
- A cart for every write. Nothing writes directly. Every change is staged, previewed against the data as it is now, and only applied with a fresh 2FA code.
- Undo for any schema. Every batch is journaled, cascades included, and can be undone - whatever the table’s primary key looks like.
- Mods. Generic features an app switches on when its tables fit: Charts, Duplicates, Map, Moderation, Leaderboard, Casino and Economy.
Why it exists
Every small app needs an admin eventually - to fix a typo in a place name, ban a user, merge two duplicates or undo a mistake. Building one per app means the same pages over and over, each with its own login and its own security holes, and most of them never get built at all.
portable-admin builds it once. And because it holds write access to every connected app, it’s strict about it: one account, required 2FA, and no write without a fresh code.
Core features
| Feature | What it does |
|---|---|
| 🛒 Cart | Every change waits here, grouped by app and previewed again before applying. Apply needs a TOTP code that’s accepted only once. |
| ↩️ Undo & redo | Any applied batch can be undone - re-inserting cascaded rows, restoring old values - and refuses if the data has moved on since. |
| 🗂️ Tables & rows | Browse, search, filter, bulk-edit and delete every table, with a dashboard of new rows per day and week. |
| 👤 Users | For apps on Better Auth: providers, sessions, sign out everywhere, and ban or lift a ban. |
| 🪣 Files | With S3: the bucket by folder, which row uses each file, and a finder for unused files. |
| 🧩 Mods | Charts, duplicates, a map, a moderation queue, leaderboards, casino and economy views - mapped to the app’s own tables. |
| ⌨️ Command palette | Ctrl/Cmd+K searches every table of an app at once. |
| 📱 Phones | The sidebar becomes a sheet and every table a list. |
Vision
The core covers any app with a Postgres database. What comes next is more mods for patterns that show up across apps, and connecting each new app as it ships instead of building it an admin of its own.
Technical details
Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router), React 19 |
| Language | TypeScript |
| UI | shadcn/ui (Radix), Tailwind CSS v4 |
| Database | postgres.js, straight to each app’s Postgres |
| Auth | Better Auth with required TOTP 2FA |
| Validation | zod |
| Deployment | Nixpacks on Coolify |
Engineering decisions
- Every write is a command. A command has a name, a zod schema and what it does. The UI only stages it; it runs only when the cart is applied with a 2FA code. A stolen session can read and stage, but not change anything.
- A journal that commits with the change. Each app’s batch runs in one transaction together with its journal entry in the app’s own
portable_adminschema - which is what makes undo safe. - Deletes include the cascade. The journal walks the foreign keys, snapshots and locks every row
ON DELETE CASCADEwill take, and stops before anything happens if a reference has no delete rule. - Rows named by their primary key, whatever it is. uuid, serial, text or composite - no
idcolumn is assumed anywhere in the core. - Identifiers only from the schema. Table and column names come from the introspected schema, never from a request; values are always parameters.
- The app owns its schema. The admin never changes an app’s tables. Its own data lives in separate schemas, and app database URLs and S3 keys are encrypted with AES-256-GCM under a key that only lives in the environment.
- Hidden means hidden. Columns that look secret never leave the server and can’t be written.
Get & use portable-admin
Deploy
git clone https://github.com/elRatto21/portable-admin.git
cd portable-admin
pnpm install
cp .env.example .env.local
pnpm dev
Set ADMIN_DATABASE_URL, ADMIN_ENCRYPTION_KEY, BETTER_AUTH_SECRET, BETTER_AUTH_URL and ADMIN_SETUP_TOKEN (see .env.example). On Coolify, deploy with Nixpacks and attach the app to each connected app’s network.
First run
- Open
/setup, enter the setup token, your email and a password. - Scan the QR code with your authenticator app and keep the backup codes.
- Under Apps → Connect an app, add a database URL and test the connection - ideally with a dedicated database role for the admin.
portable-admin - one admin for every app.