跳到內容

An app in your project

本頁內容尚未翻譯。

An app is a product Adminium hosts: tables in your database, pages in the dashboard, and optionally screens of its own for staff and for customers. The apps on adminium.dev are made this way, and you can make your own in a project.

An app of yours lives in the project’s apps/ folder, one folder per app, named after the app’s key:

my-admin/
├── adminium.config.ts
└── apps/
└── repairs/
├── manifest/
│ ├── app.json the app itself: key, name, version, publisher, sides
│ ├── tables/jobs.json one file per table
│ ├── pages/jobs.json one file per dashboard page
│ ├── roles.json
│ ├── access.json what the customer side may read and write
│ └── sample.json names the sample data file
├── staff/ screens for staff (optional)
├── customer/ screens for customers (optional)
└── seeds/sample.json sample data (optional)
Terminal window
npx @adminiumjs/adminium app new repairs --staff --customer

adminium app new writes a small working app: two tables (items, and requests for what customers send in), a dashboard page for each, one role, six sample rows, a README and a test file. --staff and --customer each add a side with one screen; leave both out for an app that is its tables and pages alone. Everything it writes is yours to edit, and nothing is generated again later.

Terminal window
npm run dev

adminium dev runs the app straight from its folder. Nothing is packed or uploaded: on start it checks each app under apps/, puts its manifest together, builds its screens and installs it on the project’s first database, with its sample data. The terminal says so:

App "repairs" installed from apps/repairs. Tables made: repairs_items, repairs_requests.
App "repairs": its sample data was added.

From then on, save a file and it is applied while the server runs. Nothing restarts:

You change What happens
a part of the manifest (tables/, pages/, roles.json, access.json, …) The app is applied again in place: new tables are made, the app’s own tables gain their new columns, its pages, roles, rules and emails are rewritten, and its rows stay. The version need not change. Open dashboards show the change
a screen (staff/src/…, customer/src/…, nav.json) The side is built again and served, and an open screen of it reloads
seeds/ Nothing: sample data is added once, on the first install
a new folder under apps/ The new app is installed

A change that cannot be applied — a column that must hold a value no two rows may share, while two rows already do — leaves the app running exactly as it was. The terminal says what stopped it, Studio marks the app Not applied with the same sentence, and the same files are not tried again until you change them (or restart). A manifest that does not check is said with its file and field, as app check would say it, and the app keeps what it had.

A screen reloads because the module it imports from, @adminiumjs/adminium/side, asks the server once a second whether the app was rebuilt. That works on a customer screen too, where nobody is signed in. A screen that should do something other than reload calls stopReloading() and passes its own listener to onAppChanged(listener). A packed app never asks.

Studio lists the app as From this project’s folder, and the folder decides what it is: Studio does not install, update or uninstall it while apps/<key>/ is there, and a package may not be uploaded under its key (KEY_IN_PROJECT). Deleting the folder does not uninstall it — a branch checked out from before the app existed would otherwise take its pages and roles with it. The app stays installed, marked Folder gone, until you uninstall it in Studio.

When the manifest no longer declares something, the next apply deals with it:

Taken out of the manifest What happens
a page Removed. A page somebody edited in Studio is kept, as an ordinary page of yours
a role Removed with its grants. The terminal says how many people and API keys held it
all of its emails, or all of its documents Removed, as an uninstall removes them
what the customer side may reach Taken back from the app’s key at once
a table or a column that holds nothing Dropped
a table or a column that holds data Kept, and asked about
a column that now holds less (a shorter text, an option taken away, a value now required) Never changed in the database. The rows that no longer fit are counted and stay as they are; new writes follow the new rule

Nothing that holds data is dropped on the way. The rest of the manifest is applied, and the question waits in Studio → Hosted apps, under the app:

apps/repairs/ no longer declares these, and they hold data. Nothing was removed.
· The column repairs_items.colour: 2 rows hold a value
[Keep the data] [Remove them]

Keep the data leaves the table or column in the database and takes it out of the app; the same manifest does not ask again. Remove them drops them, with their data, and takes a second click and a Super Admin. The same question can be read and answered without Studio: GET /api/v1/project/apps/<key>/removals, and POST the same address with { "accept": true } or { "accept": false }.

A column that stays — kept, or still waiting for its answer — would refuse every new row if it required a value, because the app no longer gives it one. Such a column is made optional in the database, which loses nothing, and the log says so. This is only ever done to a table the app made itself and uses alone; a column of a table another app or an add-on also uses is always kept.

A deployed project runs its apps the same way, from what adminium build left in .adminium/build/ — the project’s Dockerfile runs it in its build stage, and a server builds nothing. adminium start applies each app before it answers its first request, and a server is careful where adminium dev is generous:

adminium dev adminium start
Database The first one in databases, or the app’s database below The same
Public access the manifest declares Given, and the public API is switched on for it Not given, unless the config says publicAccess: true
Add-ons the app requires Installed or updated with it Must already be installed; otherwise the app is not applied
A table the app did not make itself Gains the columns the app needs Never changed: the app is not applied, and the message names the table
Sample data Added once, on the first install Never
A table or column that holds data, taken out Asked about Kept, released from the app, and said in the log
A table or column that holds nothing, taken out Dropped Kept: a server drops nothing

Which of the two a server is depends on how the process was started, never on a file: a ADMINIUM_PROJECT_MODE line in the project’s .env is ignored, so a deployed folder cannot turn a server into a developer’s machine.

An add-on an app requires has to be on the server for either of them to use it: uploaded in Studio → Add-ons, downloaded from the catalogue, or bundled — each <key>-<version>.tgz beside its .tgz.integrity in an add-ons-bundle/ folder in the project (Installing add-ons). Without it the app is not applied, and the message names the add-on.

An app needs no settings. When one does, adminium.config.ts takes them by the app’s key:

export default defineConfig({
databases: {
main: { url: env('DATABASE_URL') },
shop: { url: env('SHOP_DATABASE_URL') },
},
apps: {
repairs: { database: 'shop', publicAccess: true, sampleData: false },
},
});
Setting
database The key under databases the app’s tables live in. Default: the first
publicAccess true gives the app, on a server, the public access its manifest declares. It is the one deliberate switch: committed, reviewed, and read nowhere else. Setting it is applied on the next start, with no change to the app. Taking it out again does not take the access back: the server warns at each start that the app has access the config does not allow, and it is revoked under Settings → API
sampleData false stops adminium dev adding the sample data on the first install

On a server the public API also has to be there for a customer side to reach anything: set ADMINIUM_PUBLIC_API_ORIGINS and switch it on in Settings → API. adminium check warns about an app that declares public access the config has not allowed. A database that adminium dev once ran against keeps the access dev gave: a server started on it warns the same way.

adminium dev switches the public API on by itself for an app that declares public access, and records it in the audit log. It listens on every address of the machine unless told otherwise, so on a shared network start it with --host 127.0.0.1; the terminal says so when it applies.

While you work on the folder (adminium dev, Adminium Designer on your machine), a change to access.json takes effect when the app is applied: an entry may show one more column, or be reached by a claim instead of by anyone. A server (adminium start) keeps what its key was allowed, as it does for an update of a published app: an entry that would show more is left as it was, and the start log says which and why.

The manifest is the one document that says what the app is. In an app folder you may write it as a single manifest.json, exactly as the reference shows it, or as a manifest/ folder of smaller files. Adminium puts the parts together into the same document before it does anything with them, so nothing about the manifest itself changes — only where each field is written:

File in manifest/ Holds
app.json manifestVersion, key, name, version, publisher, license, description, categories, compatibility, capabilities, frontends, navGroups, widgets, and prefixed (the manifest’s requiredSchema.prefixed)
tables/<ref>.json One table of requiredSchema.tables. The file is named after the table’s ref
pages/<ref>.json One page of pages. The file is named after the page’s ref
roles.json roles, as the array
access.json An object with publicAccess and, when the app has them, publicKeys
emails.json An object with outbox and emailTemplates
add-ons.json addOns, as the object
sample.json An object with sampleData and, when used, seeds
settings.json settings, as the array
option-lists.json optionLists, as the object
documents.json documents, as the array
automations.json automations, as the array

Only app.json, one table and one page are required. A part you do not need is simply not there. A file that is none of these is an error, so a misspelt name cannot be silently left out, and keeping both manifest.json and manifest/ is an error too.

An app made in your own project carries the publisher local:

"publisher": { "id": "local", "name": "Local" }

It runs from your project’s folder, or installs from a file you upload. Adminium says so where it shows the app — “Made on this install” — because nobody but you has checked it. An app in a project folder must be local. Any other publisher but Adminium’s own is refused, an add-on can never be local, and a local app can neither replace an installed app from another publisher nor take the key of an app the online catalogue lists.

An app with no screens of its own — tables and dashboard pages only — declares one frontend of kind none:

"frontends": [{ "side": "staff", "kind": "none" }]
Terminal window
npx @adminiumjs/adminium app check

adminium app check puts the manifest together and validates it exactly as an install does. A problem names the file and the field it is in:

✗ apps/repairs/manifest/tables/jobs.json: columns.2.type — Invalid option: expected one of "id"|"text"|…

It then lists what the customer side may reach. That list comes from access.json alone: a table the app has but does not grant there is out of the customers’ reach, whatever the screens try.

app check also refuses one thing the manifest’s shape allows and an install does not: a table that anyone may add a row to may not also be one anyone may read, because every row could then be read by guessing ids. Grant reading and adding on different tables, as the starter does with items and requests.

Terminal window
npx @adminiumjs/adminium app try

adminium app try proves the app installs. It packs it, starts a fresh Adminium in a temp folder with an empty SQLite database, and installs the package through the same routes Studio uses. Then it opens each side, reads a table as the signed-in person, and asks the public API for what access.json grants and for what it does not:

✓ the package uploads (repairs-0.1.0.tgz, 9 files)
✓ the table check passes (2 table(s) to create)
✓ it installs: tables, pages, roles
✓ the sample data loads
✓ the staff side is served at /apps/repairs/staff/ (2 file(s) it names)
✓ the customer side can read "items", as access grants
✓ the customer side cannot read "requests", which access does not grant
✓ the customer side cannot read a table outside the app

A refusal is printed in the server’s own words. Nothing listens on a port, and nothing in your project or its database is touched.

Terminal window
npx @adminiumjs/adminium app pack

adminium app pack writes .adminium/packs/<key>-<version>.tgz and its fingerprint beside it. Install it on any Adminium from Studio → Hosted apps → Install an app: upload the file and paste the fingerprint (Installing apps). The project’s hooks and actions are not part of the package: they stay in the project.

A side is a small browser app in apps/<key>/staff/ or apps/<key>/customer/:

apps/repairs/staff/
├── src/main.tsx where it starts
├── src/… everything it imports, CSS and images included
├── nav.json its screens, for the dashboard's sidebar (optional)
└── public/ files served as they are (optional)

The manifest declares each side in frontends, with "kind": "spa"; app check refuses a side that is declared and has no code, and code that is not declared.

Terminal window
npx @adminiumjs/adminium app build

Under adminium dev the sides are built for you on every save. adminium app build bundles each side with the project’s esbuild into .adminium/build/apps/<key>/<side>/, which is exactly the folder Adminium serves at /apps/<key>/<side>/. The page it writes carries no inline script, and every asset is addressed under that mount, so a screen opened at a deep address still finds its files. React comes from the project’s own dependencies.

nav.json lists the screens a staff side offers the dashboard’s sidebar:

[
{ "id": "jobs", "path": "", "label": "Jobs", "icon": "wrench" },
{ "id": "done", "path": "done", "label": "Done" }
]

A path never starts with /: it is added to wherever the side is opened. icon is a Lucide icon name.

Each page of a side has an address of its own, and a path in nav.json is one of them without its first slash. A screen reads its page with usePath() and moves with <Link to="/done"> or go('/done'), all from @adminiumjs/adminium/side; the starter screens are two pages each. See Pages and their addresses.

A side imports its plumbing from @adminiumjs/adminium/side. The build supplies that module from the Adminium doing the building, so it always matches the server that serves the side.

A staff side runs inside Adminium, as the person who is signed in. It has no key:

import { useStaff } from '@adminiumjs/adminium/side';
const loaded = useStaff();
if (loaded.state === 'ready') {
const { rows } = await loaded.value.list('jobs', { order: 'id.desc' });
await loaded.value.create('jobs', { title: 'Fix the door' });
}

list, get, create, update and remove take a table’s short name — the ref in its part file — and act with the signed-in person’s own permissions; can(table, action) says whether a button is worth showing. The session also carries user, the app’s settings, the venue’s timezone and currency. When the database has no time zone set, timezone is UTC and timezoneIsFallback is true: a screen should say so, and must never use the browser’s zone instead.

A customer side is public. useCustomer() gives it the browser key Adminium serves and the address of the public API, and the side makes its client with @adminiumjs/public-client:

import { createPublicClient } from '@adminiumjs/public-client';
import { useCustomer } from '@adminiumjs/adminium/side';
const loaded = useCustomer();
if (loaded.state === 'ready') {
const client = createPublicClient(loaded.value);
const jobs = await client.list(loaded.value.tables['jobs'] ?? 'jobs');
}

That client reaches only what access.json grants.