Skip to content
Nelcota
PT : Ler em português

Panel

12 min read Edit on GitHub

At https://<project-domain>/admin/. Its own login, separate from end users: the email and password generated by the first nelcota init (the password is shown only once).

The panel is available in Portuguese and English. On the first visit it follows the browser language (any Portuguese locale gets Portuguese, anything else English); the choice in the account menu (Language) is remembered in that browser. Server messages and error texts are English; the panel shows errors in the chosen language by their error code.

Prerequisites

Use an initialized project with panel credentials and access to its API origin. The project administrator can run privileged SQL; this is not an end-user token or an enrolled update-operator login. This reference describes current source.

Several projects

Each project has its own panel, on its own domain, showing only its data. At the top left, next to the mascot, the project switcher (current project name) lists the other projects on the host and leads to "All projects": name, domain, state (up / down), version and a button to open each panel.

With single sign-on (default), switching projects in the switcher does not ask for a password. With per-project login (nelcota panel-login per-project), the switcher just takes you to the other panel, which asks for its own login. The login screen always shows which project the panel belongs to.

How single sign-on works

Cookies do not cross different domains (api.shop.com → api.blog.com), so the origin panel performs a handoff:

  1. The admin, already signed in to the shop's panel, picks "blog" in the switcher.
  2. The shop's panel issues a token for the blog: an HS256 JWT signed with the host's shared secret, aud = blog, valid for 60 s, unique jti.
  3. The browser opens https://<blog>/admin/#sso=<token>. The token travels in the URL fragment, which does not show up in server logs or the Referer.
  4. The blog's panel validates signature, audience, validity and the admin email, refuses already-used tokens and creates its own session. The token leaves the URL immediately.

With per-project login there is no shared secret: the handoff does not exist, and each panel only accepts its own credentials.

An SPA in Svelte 5 + shadcn-svelte + Tailwind v4, with CodeMirror 6 in the SQL editor, Manrope for the interface and IBM Plex Mono for code. The build is embedded in the binary: no CDN and no Node in production (fonts too, because of the CSP font-src 'self'). Dark theme by default; theme and language are in the account menu, at the top right.

The pages are pills across the top (on narrow screens, in their own row; on phones, in the menu). Database groups Tables, SQL and Migrations, with tabs between them inside each page; User sign-in is reached from the Users page. The project chip at the top left shows the project, whether it runs locally or on which host, and a dot for whether the server answers (checked every minute and when the window regains focus). Search (Ctrl K) sits beside the account chip. The overview leads with the API's traffic: requests in the last 24 hours, the share refused and the share that failed, then cards for tables (how many are protected), users (how many have signed in from the app) and refused calls, beside a chart of requests and refused calls over the last hour or day, with a table view. The counts are kept in memory per minute for 24 hours: restarting the server starts them again.

RLS state shows as a colored badge: green (RLS with policies), amber (RLS on without policies) and red (table exposed without RLS).

Feature How to use it
Edit a cell double click; Enter saves, Esc cancels, NULL button for nulls
Edit or insert a full row expand button next to the row's checkbox / "Insert row" (side panel). Closing an altered form asks whether to keep editing or discard
Delete several rows tick the boxes and "Delete N" (a single transaction)
Sort click the column header (asc → desc → unsorted)
Filter "Filter" in the grid: equals, not equal, contains, greater/less, is/is not NULL. Filters live in the URL in the REST API format (/admin/tables/orders?status=eq.paid). Page, page size and sort use _page, _size, _sort and _desc; the complete view survives switching between Data and Structure, refresh and browser navigation
Export a table "Export" → CSV or JSON, with the grid's order and filters. The server streams with bounded buffers and no collection row cap; exports have a two-minute lifetime and can fail on timeout/cancellation. CSV has a BOM for Excel accents
Follow a foreign key the arrow in an FK cell opens the referenced row in the other table; the header shows → table
Run SQL Ctrl+Enter; table and column autocomplete; templates and history
Save queries Ctrl+S or "Save"; they show in the editor's sidebar and in the palette. Each query has its own draft; opening another query, template or new query keeps previous work. Saved queries and drafts are stored in this browser, per project
Read long values "View full content" in long grid and SQL result cells; copy from the detail dialog
Resize SQL results drag the separator between the editor and results, or focus it and use the arrow keys. The split is remembered in the browser
Export a SQL result "Export" above each result → CSV or JSON
Command palette Ctrl+K (⌘K on Mac) or the search button at the top right: pages, tables, saved queries, templates and actions
Create a table "New table" in the table list: columns (type, default, PK, identity, UNIQUE, foreign key), RLS on by default and a GRANTs matrix. Shows the SQL before creating
Edit the structure the table's Structure tab (/admin/tables/<name>/structure): add, edit and delete columns; rename the table; description; turn RLS on/off; GRANTs per role; delete the table (by typing its name)
Policies search tables and filter their RLS status; "New policy" on each table, with templates (public read, signed-in users read, owner reads/creates/updates/deletes); edit and delete; "Enable RLS" on tables without it. Long expressions expand on demand
service_role token API page, expandable token section after the examples: issues a token with the chosen validity; shown once and never stored
Create a user / reset a password Users page: "New user" and "Reset password…" in each user's menu, with a password generator. Same rules as public signup. Accounts created here count as confirmed and the panel sends no invitations; end users can recover their own password by email when the project has SMTP configured (The auth schema)
Generate migration Migrations page: schema changes made in the panel (tables, columns, policies) that are not in any file yet, and the project's migrations (applied or only in the folder). "Generate migration" downloads V<n>__<name>.sql and registers it as applied in this database; the file goes into migrations/ (see deploy)
Profile photo account menu, at the top right: "Add photo…". The image is center-cropped and scaled to 256px in the browser; the server accepts PNG, JPEG or WebP up to 256 KB. It lives in the project's database (nelcota.admin_avatar, out of reach of the API roles), so it works in any browser, but it is per project
Dates in the grid timestamptz shows in the viewer's time zone and language; timestamp and date as stored. The exact value is in the tooltip, in edit mode and in exports

CSV exports protect spreadsheet imports: formula-like cells and headers receive a quoted tab prefix, including formulas hidden behind whitespace or control characters. Negative numeric literals keep their digits and numeric form. Use JSON export when you need the original text without this neutralization.

Page What it has
Overview what needs action (unprotected or locked tables), next steps ticked by the project's state (first table, data protected, a user, a first sign-in through the app), a one-line summary and the table list with who can read and write. Pages that show access rules read in plain language; "Technical details" shows RLS, roles, grants and SQL
Tables every table in the exposed schema, estimated rows, anon/authenticated GRANTs and RLS status. List, insert, edit and delete rows (tables with a primary key); create tables and edit the structure
SQL editor runs as the database owner (bypasses RLS, shown as a "Full access" notice), or as a visitor or a chosen signed-in user to try the access rules (D94); Ctrl+Enter runs the query, or only the selection; a failed statement is underlined in the editor; a status bar with cursor position and run limits; results fold away until the first run; drafts are named by what they do ("Query on orders"); saved queries are renamed by clicking their title
Migrations panel changes not yet in a migration and the project's migrations, with "Generate migration"
Storage buckets with file count, size, limit and accepted types (create, edit, delete when empty); a file browser by folder: upload by selection or drop, per-file progress (asks before replacing), preview, download, copy the public URL, delete. Passive image formats and PDFs up to 20 MB can be previewed; plain text, CSV and JSON up to 1 MB. PDFs open in the browser viewer. Files go through the same checks as the API (Storage)
Users search by email, how each account signs in (password, Google, GitHub, or email links only), last sign-in, active sessions, unconfirmed emails; create a user, confirm an email, reset a password (ends their sessions), end sessions or delete a user
Policies each table's policies (USING/WITH CHECK): create, edit and delete; tables without RLS; functions anon can execute
API section navigation, project address, how each role calls the API, curl and JavaScript examples generated from each table's columns, service_role token. Topic, language and table are shareable through the URL

Connecting an app and understanding access

  • API → Connect with the SDK: install command, a Download database.ts button (the same file nelcota types writes) and the createClient lines for this project. The examples switch between the TypeScript SDK (default), curl and plain fetch, generated from the chosen table's columns.
  • User sign-in: which ways in are on (sign-up, email confirmation, password recovery, magic link, Google, GitHub), and for each one that is off the environment variables that turn it on. With a provider on, it shows the callback address to register with Google or GitHub and the app pages a sign-in may return to. Read-only: these settings live in the server's .env.
  • Tables → View as: reads the grid as a visitor or as a chosen user, with the same role, claims, grants and RLS the API applies, in a transaction that is rolled back. It is read-only; a role without a GRANT gets a plain explanation instead of rows.
  • Storage → a bucket → Who reaches the files: the policies on storage.objects that apply to the bucket, in plain language, and templates to add more (anyone or signed-in users download, signed-in users upload, each user only their own folder or their own files). They are created like any panel change, so "Generate migration" includes them. A private bucket without rules is flagged: only the panel and service_role reach it.
  • Policies → Recently blocked: the last 100 API calls answered with 401, 403 or 429, who made them and why, in plain language. They are kept in memory only (gone on restart), with the path but never the query string or the token. Reads that RLS filters are not refusals (they return fewer rows) and do not show here.

Guides and references

The book icon in the top bar opens Guides and references at /admin/guides. The reader includes its registered project guides, JavaScript/TypeScript and Rust SDK references, SQL examples and server recipes. Repository-only references and historical reports are not automatically added to the reader. Search matches the full text, code blocks have copy buttons, and a table of contents links to each section. On phones, a guide selector replaces the sidebar. Guides and references follow the panel language, with Portuguese translations of the English sources. Code examples, inline code, URLs and section identifiers remain unchanged. Translations live in docs/i18n/pt-BR/; npm run guides:check verifies every prose fragment against its source and fails on missing or outdated translations. Code-only changes flow straight from the source without requiring a translation update.

SDK help on the API page and the S3 guide on the Storage page open in this reader. Links between repository documents stay in the panel, including section links that can be shared and survive a reload. Registered sources and images are bundled, so reading those needs no GitHub access. The longer Notes staging guide opens on GitHub. Some unregistered repository links are not supported by the current reader; consult the repository for them.

API → OpenAPI documentation, also available from the command palette, opens the live reference inside the panel. It fetches /rest/v1/ as anon, shows available routes, parameters, request bodies, responses and schema models, and can download the complete JSON. The panel administrator session does not grant extra API privileges. An empty catalog points to the table access screen; a failed request can be retried.

RLS warnings

  • without RLS: a regular table with a GRANT to anon or authenticated and RLS off. Whoever has the GRANT sees and changes every row. Shown prominently at the top of the panel and on the policies page.
  • RLS without policies: RLS on and no policy. Nobody but service_role (and the owner) has access: it is safe, but maybe not what you want.

Changing the admin password

shell
nelcota admin-password            # single login: new host password, applied to every project
nelcota -p shop admin-password    # per-project login: new password for the shop only

You can also use an argon2id hash (PHC format) generated by any tool: NELCOTA_ADMIN_PASSWORD_HASH='$argon2id$...' in the .env (in single quotes because of the $), followed by docker compose up -d --force-recreate app.

Security

  • Session cookie HttpOnly, SameSite=Strict and Secure behind Caddy; in-memory sessions valid for 12 h (restarting the server signs everyone out).
  • Login attempts limited to 10 per minute.
  • The page itself contains no data: everything comes from /admin/api/*, which requires the session. Every state-changing request must come from the same origin.
  • CSP without inline scripts (script-src 'self'), X-Frame-Options: DENY.
  • Svelte escapes interface text. The guide reader renders Markdown with raw HTML escaped and unsafe links rejected; code examples are rendered as text.
  • DDL made by the panel (tables, columns, policies): the interface sends a typed specification, and the server builds the SQL with identifiers always quoted and types from a closed list. Expressions (DEFAULT, USING, WITH CHECK) are SQL by nature: each statement goes through the extended protocol, which refuses a second statement hidden in the expression, and everything runs in one transaction (or nothing is applied). The SQL is shown before running.
  • Foreign keys from the form only point into the exposed schema (for auth.users, use the SQL editor).
  • The SQL editor uses a new connection per run and the SQL text does not go to the log.
  • service_role tokens issued by the panel are neither stored nor logged (only the fact and the validity).
  • Without NELCOTA_ADMIN_EMAIL and NELCOTA_ADMIN_PASSWORD_HASH, the panel is off (the /admin routes do not even exist).

Panel development

shell
cd crates/admin/ui
npm ci
npm run dev        # http://127.0.0.1:5173/admin/; proxy: http://127.0.0.1:8000
npm run check      # types (svelte-check)
npm run build      # generates dist/ (versioned and embedded in the binary)

After bundled guide/UI changes, run npm run build and rebuild the binary (cargo build --locked -p nelcota-server). Commit generated dist/ in its own build commit. CI checks it for drift. Set NELCOTA_API_URL when Vite should proxy another backend.

Interface texts live in src/lib/i18n/messages/<area>.ts, with a Portuguese (pt-BR) and an English (en) object; the English one is typed against the Portuguese one, so a missing key fails npm run check. Never hardcode a user-facing string in a component.

See deploy, auth, storage and troubleshooting.

Host projects in the controller

The Unreleased host controller adds project creation, paired backups, restore and removal with visual review, typed confirmation, operator reauthentication and durable recovery. It uses a separate HTTPS origin and SSH-granted host authority. The Projects page links to this controller when enrolled. Deployment, supported layouts and activation are described in docs/host-management.md.