{
  "access": "public",
  "type": "tutorial",
  "format": "markdown",
  "title": "Reactor Apps & Smart Panels",
  "chunked": true,
  "url": "https://library.datagrout.ai/reactor-apps",
  "summary": "Turn logic-cell rules into live applications: JSON APIs, shareable web forms, and an encrypted per-app records store — no server code, no deployment.",
  "content_markdown": "# Reactor Apps & Smart Panels\n\nTurn logic-cell rules into live applications: JSON APIs, shareable web forms, and an encrypted per-app records store — no server code, no deployment.\n\nReactor builds directly on [Logic Cells](logic-cells): a rule you can query is a rule you can serve.\n\n## The idea\n\nYou have a rule in a namespace:\n\n```prolog\nquote(Country, Product, Price) :- base_price(Product, P), vat(Country, V), Price is P * (1 + V).\n```\n\n`reactor.expose` turns it into:\n\n- a **JSON API** at `/apps/<app>/<slug>.api` — callable by anything that speaks HTTP;\n- a **Smart Panel** at `/servers/<app>/<slug>.sp` — a branded, shareable web form for the same rule.\n\nRequests bind the rule's inputs, the engine solves, outputs return as JSON (or rendered results in the panel). Every execution runs through the same sandboxed query path as `logic.query` — an exposed rule can do exactly what a queried rule can do, nothing more.\n\n## Exposing a rule\n\n```json\n{ \"rule\": \"quote\", \"namespace\": \"pricing\" }\n```\n\nThat's the common case — the **contract is auto-derived** from the rule head: the last argument becomes the output, the rest become inputs. To control it explicitly, pass a mode string:\n\n```json\n{ \"rule\": \"quote\", \"namespace\": \"pricing\", \"modes\": \"+country:atom +product:string -price:float\" }\n```\n\n`+name:type` binds from the request; `-name:type` is collected and returned. Types: `atom`, `string`, `integer`, `float`, `bool`, `any`. The resolved contract comes back in the response along with both URLs.\n\n### Verbs and safety\n\n- `verb: \"get\"` — cacheable, prefetchable; for pure queries.\n- `verb: \"post\"` — for anything else.\n\nRules whose body calls `call_tool`, `assertz`, or `retract` are **forced to POST** regardless of what you request — side-effecting endpoints must not be GET-cacheable. This is detected from the rule body, not trusted from the caller.\n\n### Auth\n\n`auth`: `\"none\"` (public), `\"bearer\"` (token in the `Authorization` header; a token is minted and returned at expose time), or `\"password\"` (for panels — a shared secret gate on the form).\n\n### Shaping the endpoint\n\n- `rate_limit_per_hour` — throttle per caller.\n- `limit` — cap solutions returned per request.\n- `single: true` — return the first solution as an object instead of a list.\n- `requires` — battery ids to hydrate before serving (usually auto-detected).\n\n### Battery dependencies\n\nIf the rule depends on battery predicates, expose hydrates the declared batteries into the namespace automatically — the endpoint works on first call.\n\n## The records store\n\nReactor apps get an **encrypted records store** for state that doesn't belong in the fact space — submissions, orders, profiles:\n\n| Tool | Does |\n|---|---|\n| `store.put` | Upsert a record by key (payload encrypted at rest) |\n| `store.get` | Fetch one record (decrypted) by key |\n| `store.query` | List/filter by plaintext metadata containment (e.g. `{country: \"be\"}`), newest first |\n| `store.delete` | Remove a record by key |\n\nRecords are scoped to the calling server, an optional `site`, and a named `collection` — one app can keep `orders` and `subscribers` separate, and one server can back multiple sites. Payloads are encrypted; the `metadata` you choose to attach is plaintext and is what `store.query` filters on — put only what you're willing to index there.\n\nA common pattern: the exposed rule handles the *decision* (validate, price, classify) and a follow-up `call_tool` to `store.put` inside the rule persists the outcome — the whole app is one rule plus the store.\n\n## Smart Panels\n\nSmart Panels are the human-facing surface: a branded form generated from the endpoint's contract, served at the `.sp` URL, submitting to the rule and streaming results back. Panels are built and customized in the **Panel Studio** (field labels, ordering, branding, result presentation) and shared by URL — visitors don't need a DataGrout account (subject to the endpoint's `auth` setting).\n\nPanels render the same contract the API serves: one exposure, two surfaces. Anything a panel can submit, the API accepts, and vice versa.\n\n## Availability\n\nReactor and Smart Panels are feature-gated (per-account, or enabled site-wide by the platform). Having Reactor implies Smart Panels — a reactor app is never left API-only. If `reactor.expose` reports the feature is unavailable, ask your admin.\n\n## Quick reference\n\n| Setting | Where | Values |\n|---|---|---|\n| Rule + namespace | `reactor.expose` | any stored rule |\n| Contract | `modes` | `+in:type` / `-out:type`, auto-derived if omitted |\n| Verb | `verb` | `get` / `post` (side-effecting rules forced to `post`) |\n| Auth | `auth` | `none` / `bearer` / `password` |\n| API URL | response `url` | `/apps/<app>/<slug>.api` |\n| Panel URL | response `panel_url` | `/servers/<app>/<slug>.sp` |\n| Store scoping | `store.*` | server + optional `site` + `collection` |\n| Store filtering | `store.query` | plaintext `metadata` containment |\n"
}