PHPackages                             lumnd/platoadmin - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. lumnd/platoadmin

ActiveProject[API Development](/categories/api)

lumnd/platoadmin
================

Back office scaffold on PlatoPHP: an admin application plus a contract-generated JSON API

v0.1.1(today)04↑2900%MITPHPPHP ^8.2CI passing

Since Aug 24Pushed todayCompare

[ Source](https://github.com/lumnd/platoadmin)[ Packagist](https://packagist.org/packages/lumnd/platoadmin)[ RSS](/packages/lumnd-platoadmin/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (12)Versions (3)Used By (0)

PlatoAdmin
==========

[](#platoadmin)

**English** · [中文](README.zh-CN.md)

A back office scaffold on [PlatoPHP](https://platophp.com), and the reference application for it: everything the framework documentation describes in the abstract is here as code that runs.

Two applications share one tree. `admin` renders Smarty templates; `api` is a JSON service whose controllers are generated from the contracts in `api/contracts/`. Both sit on the models, services and language packs under `common/`. Start a project from it and the parts every back office rewrites from scratch — accounts, roles, permissions, sessions, logs, menus, settings, scheduled tasks — are already there.

MIT licensed, like the framework.

 [![The dashboard: account and sign-in counters over the recent sign-in log](docs/assets/screenshots/dashboard.png)](docs/assets/screenshots/dashboard.png)

  [![Roles, each with the number of actions it grants and the number of accounts in it](docs/assets/screenshots/roles.png)](docs/assets/screenshots/roles.png) [![Accounts, with role, status, two-step verification and last sign-in](docs/assets/screenshots/accounts.png)](docs/assets/screenshots/accounts.png)   **Roles** — a role is a set of `ct:ac` permissions **Accounts** — status and two-step state at a glance   [![The operation log: who did what, tied to the session that did it](docs/assets/screenshots/oplog.png)](docs/assets/screenshots/oplog.png) [![Live sessions, each of which can be ended](docs/assets/screenshots/sessions.png)](docs/assets/screenshots/sessions.png)   **Operation log** — no delete; pruning is by age and is itself logged **Sessions** — the authority is the database row, not the cookie Screens shown with seeded demo data. More: [docs/assets/screenshots/](docs/assets/screenshots/)

**Try it: ** — sign in as `demo` / `platoadmin`.

The demo signs in to a read-only role, so every screen and every form opens and nothing writes; it resets to the seeded state every half hour. Two behaviours you will meet and which are not faults: a write comes back 403, and opening the form of a role that holds permissions the demo does not is refused, because an account cannot grant what it does not hold itself.

What is in it
-------------

[](#what-is-in-it)

**Back office** (`admin`)

- **Accounts and roles** — a role is a set of permissions, written as `ct:ac` / `ct:*` / `*`. An account cannot grant a permission it does not hold itself.
- **Sign-in** — password plus optional two-step verification, rate limited per source address. An account can be made to change its password on first use, restricted to a set of addresses, or given an expiry date.
- **Sessions** — the authority is the database row, not the cookie, so any live session can be signed out from the list and a suspension takes effect on the next request.
- **Logs** — a sign-in log and an operation log, with each operation tied to the session that performed it. Neither has a delete: pruning is by age and is itself logged.
- **Menu** — a tree, maintained in the back office, whose items line up with controller actions.
- **Settings and scheduled tasks** — tasks are defined in configuration; the screen can disable and restore one and shows how long each run took and whether it succeeded.

**API** (`api`)

- The auth chain: `send_code`, `login`, `refresh`, `logout`.
- Sign-in by email code, SMS code, Google or Apple. Deliberately no password sign-in.
- The access token is a JWT (HS256, two hours). The refresh token is stored as a sha256 digest, used once, and rotated.
- Controllers and the OpenAPI document are both generated from `api/contracts/`. The code a person writes lives in `api/app/logic/`.

**Shared by both**

- One failure mechanism: throw `biz_exception`. `common\middleware\catcher` is the only place that turns an exception into a response — there is no sentinel return value and no error map.
- Deterministic field encryption (so an encrypted column can still be matched on equality), soft deletes, and one response envelope: `{code, msg, data, timestamp}`.
- Language packs for `zh-cn` and `en`, phpstan level 8, Pest tests.

What is deliberately not in it
------------------------------

[](#what-is-deliberately-not-in-it)

Not omissions — decisions, listed so nobody goes looking:

- **No product domain.** User profiles, content, subscriptions and payment are not settled, so the main tree has neither their tables nor their endpoints.
- **The SMS channel is a stub.** `common\integration\sms` sends nothing, and an SMS code request answers honestly that it could not be sent rather than pretending it went out.
- **No resident server runs.** The full profile installs `lumnd/plato-workerman` and registers the `server:*` commands, but there is no `config/server.php` and no server implementation.
- **Two-step verification validates but does not enrol.** An account with `otp_enabled` is asked for a code at sign-in; the screen that generates a secret and confirms a scan is not written yet.
- **The Google and Apple adapters have never seen a real token.** They are verified as far as refusing a deployment that has no client id configured.

Quick start
-----------

[](#quick-start)

With Docker, from nothing to a back office you can sign in to:

```
composer create-project lumnd/platoadmin myapp && cd myapp
docker compose up -d
docker compose exec php83 composer install
docker compose exec php83 cp .env.example .env   # then set DB_PASSWORD=root
docker compose exec php83 php vendor/bin/plato key:generate
docker compose exec php83 php vendor/bin/plato migrate
docker compose exec -it php83 php vendor/bin/plato admin:create --username=admin --super
```

Then open . **No hosts file entry is needed** — the `.localhost` suffix is reserved for the loopback address and resolvers answer it on their own.

The compose stack is PHP 8.3-fpm, nginx, MySQL 8 and Redis 7, and it is for development only: the source is a bind mount, opcache is off and the database password is in the compose file in plain sight. [docs/running.md](docs/running.md) covers it properly, including how to use an nginx you run yourself instead.

Requirements
------------

[](#requirements)

Without Docker, running it yourself needs:

- PHP **8.2** to run it, **8.3** to develop it. 8.2 is the floor `composer.json` states and CI proves with a `composer install --no-dev` job; the dev toolchain needs 8.3, because Pest does
- Composer 2
- MySQL 8.0
- Redis
- Extensions: `pdo_mysql`, `redis`, `mbstring`, `openssl`, `json`, `zlib`; a resident process also needs `pcntl` and `posix`

Install
-------

[](#install)

```
composer create-project lumnd/platoadmin myapp
```

### Profiles

[](#profiles)

The install asks which profile you want, deletes what the answer excludes, and removes itself:

ProfileWhat it is`minimal`Back office only. No `api/`, no contract generation, no resident server. The dependencies come down to `lumnd/platophp` plus `smarty/smarty` and `chillerlan/php-qrcode``dsl`Adds `api/`, where endpoints are declared in `api/contracts/` and both the controllers and the OpenAPI document are generated`full`Adds resident server support through `lumnd/plato-workerman`Answer up front to skip the question, and add `--dry-run` to see what would go without touching anything:

```
php install.php --profile=minimal --dry-run
```

Configure
---------

[](#configure)

```
cp .env.example .env
php vendor/bin/plato key:generate
```

`key:generate` fills the three secrets that have to differ in every installation. It only writes the lines that are empty, so running it twice is safe:

- `CSRF_SECRET` — signs the CSRF cookie. An empty value makes the back office refuse to start.
- `JWT_SECRET` — signs api access tokens. Replacing it signs every api client out at once, which is the emergency stop when one leaks.
- `DB_CRYPT_KEY` — field encryption. **There is no recovery path if it is lost**, only re-encryption from a plaintext source, so it must never be swapped out under an environment that already has data.

To replace one that already has a value, name it: `key:generate --force=JWT_SECRET`, which asks first. `--show` prints without writing.

`SYS_ENV` is one of `dev` / `pre` / `pub`. `SYS_DEBUG` must be `false` in production — the debug panel prints the session and the cookies verbatim.

`.env` goes in no repository, no image and no release archive. Only `.env.example` and `.env.testing` are committed, and neither holds a real credential.

Run
---

[](#run)

```
php vendor/bin/plato migrate
php vendor/bin/plato admin:create --username=admin --super
```

**There is no seeded administrator, and there will not be one.** An account committed to a repository means the password is in the source and every deployment that ran the seeder shares it. In a container, pass `-it` to `docker exec` — without a tty the prompt cannot turn echo off and the password ends up in the shell history.

The two applications are two document roots: `admin/public` and `api/public`. The vhosts are in `deploy/nginx/` — the compose stack mounts that directory straight into nginx, and the same files copy into an nginx you run yourself. The crontab entry is in `deploy/cron/`.

CSRF compares `Origin` against the request host and the cookie is set on that host, so **serve it under a hostname rather than with `php -S`** — `127.0.0.1:8080` works, but it is not the path production takes. A non-default port is fine: `Origin` and `HTTP_HOST` both carry it, so the two still match.

The full walk-through — the compose stack, bringing your own nginx, the database, the first account, the debug panel and a symptom table — is in [docs/running.md](docs/running.md).

Layout
------

[](#layout)

WhereWhat`admin/`Back office application, entry point `admin/public/index.php``api/`API application, entry point `api/public/index.php`; controllers generated from `api/contracts/``common/`Models, services, support and integrations shared by both, plus the language packs`database/migrations/`Schema and upgrades, the only source of the table structure`database/seeders/`Built-in data (the menu tree). **No accounts**`deploy/`The nginx vhosts, the crontab entry, and the images the compose stack builds`docker-compose.yml`The development stack: PHP 8.3-fpm, nginx, MySQL 8, Redis 7`docs/`Project documentation`vendor/lumnd/platophp/`The framework, a Composer dependency. **Never edited**What belongs in each directory, and where new code goes, is [docs/structure.md](docs/structure.md). The layering and writing rules are [docs/conventions.md](docs/conventions.md).

The framework's own documentation is at . It is **not** in `vendor/` after a normal install — the published tags strip `docs/` with `.gitattributes`, so the zipball Composer unpacks does not carry it. To read it offline, fetch that package from source instead:

```
composer reinstall lumnd/platophp --prefer-source
```

which puts it at `vendor/lumnd/platophp/docs/`, entry point `llms.txt`.

Commands
--------

[](#commands)

```
php vendor/bin/plato key:generate
php vendor/bin/plato migrate
php vendor/bin/plato migrate:status
php vendor/bin/plato admin:create --username=x --super
php vendor/bin/plato admin:password --username=x
```

Endpoints change in the contract and are then regenerated. **Do not hand-edit `api/app/control/`** — `api/manifest.json` will stop you:

```
vendor/bin/plato api:lint
vendor/bin/plato api:generate
vendor/bin/plato api:check
```

Scheduled tasks are driven by a single crontab entry on one host; locally the scheduler does not run by default:

```
php vendor/bin/plato schedule:list
php vendor/bin/plato schedule:run --force
php vendor/bin/plato schedule:exec --task=prune:session
```

Tests and static analysis:

```
composer style     # phpcs: zero errors is the gate; the line length limit stays a warning
composer analyse   # phpstan level 8
composer test      # Pest; the Feature suite needs a real database and skips itself without one
```

Documentation
-------------

[](#documentation)

FileAnswers[docs/running.md](docs/running.md)How to run it, run the tests, and create the first account[docs/structure.md](docs/structure.md)Which directory holds what, and where new code goes[docs/conventions.md](docs/conventions.md)Layering, exceptions, error codes, routing and auth, secret handling[docs/api-contract.md](docs/api-contract.md)How a contract is written, what it generates, which files are hand-written[docs/schema.md](docs/schema.md)Table conventions and the tables that exist[docs/frontend.md](docs/frontend.md)Back office front-end conventions[docs/storage.md](docs/storage.md)File storage, uploads and bucket configuration[docs/api/openapi.json](docs/api/openapi.json)The endpoint list, generated from the contractsThe documentation is written in Chinese and published at ****, which is the readable copy — it has a sidebar, a pager and a Swagger UI page for the api. `docs/*.md` reads fine on GitHub as it is; the same sources also build into a navigable static site with a Swagger UI page for the api. **`docs/site/`is not committed** — CI regenerates and publishes it on every push, so changing documentation means changing Markdown, never HTML:

```
php docs/build.php          # writes docs/site/
php docs/build.php --check  # renders and validates structure, section counts and relative links
```

The build needs Node (through `npx marked`, pinned so two machines produce the same HTML). The Swagger UI assets are vendored in `docs/assets/swagger/` rather than loaded from a CDN.

Working on PlatoAdmin itself
----------------------------

[](#working-on-platoadmin-itself)

A plain checkout never triggers `post-create-project-cmd`, so `install.php` does not run and you get the whole tree:

```
git clone https://github.com/lumnd/platoadmin.git
cd platoadmin
docker compose up -d
docker compose exec php83 composer install
```

[CONTRIBUTING.md](CONTRIBUTING.md) has the rest: the three gates, what gets a change sent back, and the style and language rules. Security problems go through [SECURITY.md](SECURITY.md) rather than the issue tracker.

Three things worth knowing before the first pull request:

- **When you develop against a sibling checkout of the framework, do not let the lock file out.**Everything resolves from Packagist normally, so `composer.json` carries no `repositories` at all; working on the framework means adding a local `type=path` entry pointing at the sibling checkout. Everyone remembers that line must not be committed, but the `composer.lock` it produces must not be either: the `lumnd/*` entries get written as `"type": "path"` with `dev-main`, which contradicts the `~0.1` constraint in `composer.json` and stops somebody else's `create-project`dead with `does not satisfy your constraint`. After removing the path repository, run `composer update lumnd/platophp lumnd/plato-api-contract lumnd/plato-workerman` and check that the lock is back on the tags Packagist resolves before committing it.
- **CI runs the three gates and builds the documentation.** `.github/workflows/ci.yml` runs style, static analysis and the full test suite against a real MySQL and Redis, plus a second job that does `composer install --no-dev` on PHP 8.2 so the floor in `composer.json` is a tested claim. `.github/workflows/docs.yml` renders the Markdown to GitHub Pages; `.gitlab-ci.yml` does the same on GitLab Pages.
- **Every change has to be thought about in three profiles.** A new file that only serves the api or only the resident server has to be registered in `PROFILES` in `install.php`, or the minimal profile ends up with a file referencing a class that was deleted. Verify all three afterwards, not just the one you touched.

The release process — build an artefact, migrate, move a symlink, restart the processes — is not implemented. The intended shape is `current -> releases/` alongside a `shared/`, with the production `.env` written into `shared/.env` by a secret service or a protected CI variable.

Lines that do not get crossed
-----------------------------

[](#lines-that-do-not-get-crossed)

- No account, password, token or certificate in plaintext, in the README, in configuration or in a log.
- **The `setting` table never holds a secret, a connection string or a third party credential** — databases get exported, backed up, and copied into development environments.
- Production runs `SYS_ENV=pub` and `SYS_DEBUG=false`.
- The framework is a Composer dependency: **do not edit `vendor/`**, the next `composer install`takes it away. Framework bugs get fixed in the framework repository.

License
-------

[](#license)

MIT, see [LICENSE](LICENSE). PlatoPHP is MIT as well.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

PHP version history (2 changes)v0.1.0PHP ^8.3

v0.1.1PHP ^8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/249724787?v=4)[nbjames81](/maintainers/nbjames81)[@nbjames81](https://github.com/nbjames81)

---

Top Contributors

[![nbjames81](https://avatars.githubusercontent.com/u/249724787?v=4)](https://github.com/nbjames81 "nbjames81 (14 commits)")

---

Tags

admin-dashboardadmin-panelbackendboilerplateopenapiphpphp8platophprbacscaffoldsmartyapiscaffoldbackendadminplatophp

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/lumnd-platoadmin/health.svg)

```
[![Health](https://phpackages.com/badges/lumnd-platoadmin/health.svg)](https://phpackages.com/packages/lumnd-platoadmin)
```

###  Alternatives

[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.7k39.6k](/packages/matomo-matomo)[plivo/plivo-php

A PHP SDK to make voice calls &amp; send SMS using Plivo and to generate Plivo XML

1113.2M19](/packages/plivo-plivo-php)[plivo/php-sdk

A PHP SDK to make voice calls &amp; send SMS using Plivo and to generate Plivo XML

1112.1M6](/packages/plivo-php-sdk)[doppar/framework

The Doppar Framework

4112.6k14](/packages/doppar-framework)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
