Sophia Stack X

Building extensions

An extension is a folder. It can add content types, page blocks, modules, a whole template, data models with admin screens, web routes and background reactions to events. Server code is JavaScript in a sandbox. There is no Node, no npm and no build tool to install.

Start one

From a folder that has an extensions directory:

sophia new app my-app            # also: block, component, template
sophia check extensions/my-app   # add --json for a machine-readable report

check validates the manifest, parses every template and compiles server.js. Then switch the extension on for a site under Extensions, or with the extension_enable tool.

The folder

extension.json      the manifest
templates/*.html    Jinja templates; names are "<extension>:<path>"
server.js           optional server code
css/, js/, img/     sources bundled by "sophia build" (minified and fingerprinted)

After editing, run sophia check and sophia build, then restart or press Reload extensions.

The manifest

Every key is optional except name:

{
  "name": "tickets",
  "title": "Tickets",
  "version": "0.1.0",
  "kind": "app",
  "description": "A small support desk.",
  "server": "server.js",
  "permissions": ["data", "mail"],
  "collections": [{ "key": "faq", "label": "FAQs", "fields": [{ "name": "answer", "label": "Answer", "type": "richtext" }] }],
  "models": [{ "key": "ticket", "label": "Tickets", "title_field": "subject",
               "fields": [{ "name": "subject", "label": "Subject", "type": "text", "required": true }] }],
  "blocks": [{ "type": "promo", "label": "Promo", "template": "blocks/promo.html", "fields": [] }],
  "modules": [{ "type": "hours", "label": "Hours", "template": "modules/hours.html", "fields": [] }],
  "admin": [{ "key": "queue", "label": "Support queue", "perm": "content.edit" }],
  "settings": [{ "name": "notify", "label": "Email me at", "type": "email" }]
}

Field types: text, textarea, richtext, number, toggle, select, image, file, url, date, color, code, email, secret, list (with its own fields) and item (a link to another entry). Saved values are always cleaned against the fields.

sophia schema prints the full JSON Schema of the manifest.

Templates

Templates use Jinja syntax. They see the site, the page, menus, the current entry or list, block and module settings, and the template's own settings as theme. Helpers include position(), asset(), link(), t() for translated strings, render_blocks(), and the filters excerpt, date, time12, tel and money.

Server code

sx.route("GET", "/open", (req) => ({ json: sx.data.list("ticket", { where: { status: "open" } }) }));

sx.route("POST", "/new", (req) => {
  if (!sx.rateLimit("ip:" + req.ip, 5, 3600)) return { status: 429, text: "Slow down." };
  sx.data.create("ticket", { subject: String(req.form.subject || "").slice(0, 200), status: "open" });
  return { redirect: "/x/tickets/thanks" };
});

sx.admin("queue", (req) => ({ html: sx.render("queue.html", { rows: sx.data.list("ticket", {}).items }) }));

sx.on("record.saved", (event) => sx.log("saved", event));

Routes answer at /x/<extension>/<path>. A route returns { json }, { html }, { text }, { page: { title, html } } (wrapped in the site's layout) or { redirect }, with optional status and headers.

The request carries method, path, params, query, body, json, form, headers, ip, the signed-in site account and the site.

What code may call depends on its declared permissions:

CallPermission
sx.data.list / get / create / update / removedata
sx.content.list / getcontent.read
sx.content.draft (an editor publishes it)content.write
sx.accounts.get / list / setRolesaccounts
sx.http.fetch(url, ...)http:<host> for each host
sx.mail.send({ to, subject, text })mail

Always available: sx.settings(), sx.site(), sx.render(), sx.escape(), sx.log(), sx.rateLimit() and sx.requireRole(req, "staff"). Each call runs fresh with a memory and time limit (64 MB, 3 seconds by default).

Live page regions

Parts of a page can update from the server without a reload, with no framework and no build step. Include the core script and mark the region:

<div id="queue" data-sx-live="/x/tickets/queue" data-sx-every="15"></div>
<form data-sx-target="#queue" method="post" action="/x/tickets/close">...</form>
<a data-sx-target="#detail" href="/x/tickets/item/4">Open</a>
<script src="{{ asset('core:live.js') }}" defer></script>

data-sx-live loads the region from a route that returns { html }, and data-sx-every refreshes it every so many seconds while the tab is visible. A form or link with data-sx-target sends through fetch and swaps the answer into the target. data-sx-swap="outer" replaces the element itself. The route sees req.live as true, so one route can answer a whole page or just the part.

Rules worth keeping

  • Escape anything a visitor typed with sx.escape before putting it in HTML.
  • Declare only the permissions you use. Secrets go in settings fields of type secret.
  • Content written from code is saved as a draft. People publish.

Packing and signing

sophia keygen my.key
sophia pack extensions/my-app --key my.key
sophia trust <public key>        # on the install that will accept it
sophia install packages/my-app-0.1.0.sxp

A package installs only when a key that install trusts signed it.

The help desk extension that ships with Sophia Stack X is a complete example of an app with models, admin screens, routes and email.