Skip to main content

Hello Invoice

The official first application of AUSUS 2.0. It is a small invoice manager that walks through the entire Gen2 pipeline — Authoring → Compiler → immutable graph → Runtime → HTTP API → React Renderer — using only the public packages. It is exactly the project you would download from GitHub: no monorepo, no path repositories, no internal code.

1. Introduction

You declare an Invoice entity as data. The Entity Engine compiles that declaration into a frozen, content-addressed schema and runs it: storage, data-aware authorization, an HTTP API, and a React UI — all derived from the one declaration. You should reach a working application in about fifteen minutes.

2. Installation

Requirements: PHP 8.3+ and (for the UI) Node 18+. Create a project folder and install the public Composer packages from Packagist:

mkdir hello-invoice && cd hello-invoice
composer require ausus/authoring:^2.0 ausus/entity-engine:^2.0 \
ausus/persistence-memory:^2.0 ausus/api-runtime:^2.0

ausus/authoring provides the DSL, ausus/entity-engine the Compiler and runtime, ausus/persistence-memory the reference driver, and ausus/api-runtime the HTTP surface. ausus/kernel is pulled in automatically.

3. Create the project

hello-invoice/
composer.json
entities/
Invoice.php # the declaration
bin/
demo.php # compile → runtime → API, in one script
server.php # HTTP front controller for the React renderer
web/ # the React UI (added in step 8)

4. Declare the Invoice entity

entities/Invoice.php returns exactly one immutable EntityDefinition. Fields, actions, projections, and authorization are all data. Guards use primitive operators only (eq / lt / not): the runtime denies on any unresolved fact.

<?php
use Ausus\Authoring\Dsl\Definition;
use Ausus\Authoring\Dsl\Expr;
use Ausus\Definition\Enum\ActionKind;
use Ausus\Definition\Enum\FieldType;

return Definition::make('invoice', true) // tenant-scoped
->field('number', FieldType::String)
->field('customer', FieldType::String)
->field('issueDate', FieldType::Date)
->field('dueDate', FieldType::Date)
->field('status', FieldType::Enum, [
'default' => 'draft',
'writeProtected' => true, // only transitions change it
'typeOptions' => ['values' => ['draft', 'paid', 'cancelled']],
])
->field('total', FieldType::Decimal)

->action('create', ActionKind::Create, [
'inputs' => ['number', 'customer', 'issueDate', 'dueDate', 'total'],
'guard' => Expr::eq(Expr::actor('type'), 'user'),
])
->action('update', ActionKind::Update, [
'inputs' => ['customer', 'dueDate', 'total'],
'guard' => Expr::eq(Expr::actor('type'), 'user'),
])
->action('pay', ActionKind::Transition, [
'guard' => Expr::not(Expr::lt(Expr::subject('total'), 1)), // total >= 1
'transition' => ['field' => 'status', 'from' => 'draft', 'to' => 'paid'],
])
->action('cancel', ActionKind::Transition, [
'guard' => Expr::eq(Expr::actor('type'), 'user'),
'transition' => ['field' => 'status', 'from' => 'draft', 'to' => 'cancelled'],
])

->projection('board', ['fields' => [
['field' => 'number'], ['field' => 'customer'], ['field' => 'status'], ['field' => 'total'],
]])
->projection('detail', ['fields' => [
['field' => 'number'], ['field' => 'customer'], ['field' => 'issueDate'],
['field' => 'dueDate'], ['field' => 'status'], ['field' => 'total'],
]])
->build();

5. Compilation

The Compiler turns the declaration into a content-addressed EntitySchema (canonical normal form + SHA-256 hash). Same semantics ⇒ same hash; the runtime never recompiles.

use Ausus\Engine\Compile\Compiler;
use Ausus\Engine\Repository\InMemorySchemaRepository;

$invoice = require __DIR__ . '/../entities/Invoice.php'; // EntityDefinition
$graph = (new Compiler())->compile([$invoice]); // immutable CompiledGraph

$repo = new InMemorySchemaRepository();
foreach ($graph->schemas as $schema) {
$repo->putByHash($schema); // content-addressed store
}

6. Start the runtime

Bind the schema to a driver and invoke actions. The runtime depends only on the PersistenceDriver contract; here we use the reference in-memory driver.

use Ausus\Engine\Runtime\DefaultEntityEngine;
use Ausus\Engine\Runtime\DefaultAuthorizationEvaluator;
use Ausus\Persistence\Memory\MemoryDriver;
use Ausus\Api\Runtime\Http\RequestContextFactory;

$engine = new DefaultEntityEngine(new DefaultAuthorizationEvaluator(), $repo);
$driver = new MemoryDriver();
$ctx = (new RequestContextFactory(new DateTimeImmutable()))
->fromHeaders(['X-Tenant-ID' => 'acme', 'X-Actor-Type' => 'user']);

$invoice = $engine->bind($repo->resolve('invoice'), $driver);
$created = $invoice->invoke('create', [
'number' => 'INV-001', 'customer' => 'Globex',
'issueDate' => '2025-01-10', 'dueDate' => '2025-02-10', 'total' => 1500,
], $ctx);
$id = $created->reference->identityHandle;

$invoice->invoke('update', ['id' => $id, 'total' => 1800], $ctx); // patch fields
$invoice->invoke('pay', ['id' => $id], $ctx); // draft → paid

A guest actor calling create is denied — the guard actor.type = user fails closed. Run the whole thing with php bin/demo.php.

7. Start the HTTP API

The API Runtime exposes the same domain over a framework-agnostic contract: dispatch(method, path, headers, body) returns { status, body }. Routes: GET /api/entities/{entity}, GET …/projections/{projection}, POST …/actions/{action}.

use Ausus\Api\Runtime\Http\RuntimeApi;

$api = new RuntimeApi($repo, $engine, $driver, new RequestContextFactory(new DateTimeImmutable()));
$res = $api->dispatch('GET', '/api/entities/invoice/projections/board',
['X-Tenant-ID' => 'acme', 'X-Actor-Type' => 'user']);
// $res === ['status' => 200, 'body' => ['rows' => [ … ]]]

bin/server.php wires this into a front controller; serve it:

php -S 127.0.0.1:8080 bin/server.php
curl http://127.0.0.1:8080/api/entities/invoice/projections/board

8. Connect the React Renderer

The renderer speaks the HTTP contract only — give it a base URL and it discovers the entity, projections, and actions. Install the public npm package:

cd web
npm install @ausus/react-renderer react react-dom

web/src/App.tsx:

import { RuntimeClient, RendererApp } from '@ausus/react-renderer';

const client = new RuntimeClient({ baseUrl: 'http://127.0.0.1:8080' });

export default function App() {
return <RendererApp client={client} entities={['invoice']} />;
}
npm run dev # open the printed URL with the API server running

9. The result

You have a working invoice manager: a list of invoices (the board projection), a details view (detail), auto-generated forms for create / update / pay / cancel, tenant isolation on every read and write, and authorization enforced before anything changes — none of which you wrote by hand. Adding a field or an action to entities/Invoice.php and recompiling makes it appear in the API and the UI with no other code change.

10. What you just learned

  • An application is data: one immutable EntityDefinition.
  • The Compiler freezes it into a content-addressed EntitySchema.
  • The Runtime binds that schema to a driver and runs actions, with data-aware, fail-closed authorization and structural multi-tenancy.
  • The API Runtime exposes it as { status, body }, and the React Renderer draws it from that contract alone.
  • The whole thing runs on public packages only — the exact experience an external developer gets.

Limitations

The reference ausus/persistence-memory driver lives for one process. Under php -S, each request runs the router fresh, so writes do not persist across requests; bin/server.php seeds a couple of invoices at boot so reads always show data. For a persistent server, bind a persistent PersistenceDriver in place of MemoryDriver — the rest of the application is unchanged.