Data Store

Overview

The data store gives plugins a write path for user-generated content (UGC) that is separate from the CMS content directory. Data is stored in a _data/ directory as JSON files, organized by namespace and key.

Publisher mutation guards block writes to content/ but allow writes to _data/, so UGC works in all server roles.

Configuration

data:
  enabled: true
  directory: "./_data"     # default
  rate_limit: 60           # max writes per minute, default: 60

Plugin Access: the host-services capability

Plugins read and write the store through the typed host-services capability. The WIT world imports host-services and the manifest opts in with the data scope:

[plugin]
host_services = ["data"]

The plugin then calls data-write / data-read directly:

use bindings::accent::plugin::host_services::{self, WriteMode};

host_services::data_write("post-123", &comment_json, WriteMode::Append)?;
let stored = host_services::data_read("post-123")?;

The namespace is the plugin’s own name. A plugin named accent-comments reads and writes under _data/accent-comments/ – the host derives the namespace from the calling plugin’s identity, so there is no namespace parameter and no way to touch another plugin’s data. Values cross the boundary as the contract’s typed json arena, and failures come back as a typed host-error (not-permitted, not-configured, invalid-request, rate-limited, internal). Because no bound listener is involved, the capability also works during accent build.

Key rules: lowercase alphanumeric, hyphens, and underscores only (a-z, 0-9, -, _). Max 128 characters.

Migrating from the removed HTTP endpoints

Earlier releases also exposed two localhost-only HTTP endpoints, /_internal/data/write and /_internal/data/read, which plugins reached by allowlisting 127.0.0.1 and POSTing to their own server. Those endpoints are gone, along with the outbound-HTTP address-guard exemption that made them reachable and the server_port value the host used to inject into plugin config.

A plugin still written that way loads, but every call it makes is refused by the address guard, and the server warns at load time about the loopback allowed_hosts entry. To migrate:

  • declare host_services = ["data"] in the manifest and drop the allowed_hosts entry;
  • call data-write / data-read instead of building an HTTP request;
  • drop the namespace argument. The capability derives the namespace from the plugin’s own name, which is what makes cross-plugin access impossible.

If your plugin wrote under a namespace that is not its manifest name, that rename has three consequences. The first two are silent if missed – nothing errors, the data simply stops being found. The third is caught at startup:

  1. Move the directory: mv _data/<old-namespace> _data/<plugin-name>.

  2. Update every load_data() call in your templates from load_data("<old-namespace>", key) to load_data("<plugin-name>", key). A read from a namespace that does not exist returns nothing rather than failing, so a missed call renders an empty list on every page – no template error, no log line, and a green build.

  3. Check the plugin’s name is a legal namespace. It has to satisfy the same segment rules as a key – lowercase a-z, 0-9, -, _, at most 128 characters – because it is now a directory name. A plugin called Newsletter or acme.forms was fine when the namespace came from the request body; as a derived namespace it is not usable at all.

    This one does not fail silently: a plugin that declares host_services = ["data"] with a name that cannot be a namespace is refused at load, with a startup error naming the rule. Rename the plugin (and its directory under plugins/), or drop the "data" scope. A plugin that never asked for the data store keeps whatever name it likes.

Template Function

Use load_data(namespace, key) in Jinja templates to read stored data:

{% set comments = load_data("comments", page.slug) %}
{% if comments %}
  <h3>Comments ({{ comments | length }})</h3>
  {% for comment in comments %}
    <div class="comment">
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text }}</p>
    </div>
  {% endfor %}
{% endif %}

When the key does not exist, load_data() returns an empty array [].

load_data() works in accent serve and in accent build (previously serve-only), so a data-driven site renders the same pages as static output. Both require data.enabled: true; a build whose template calls load_data() while the store is disabled fails loudly rather than emitting pages with the data silently missing. The Accent CMS Hub catalog is built exactly this way: content stubs carry only identity, and every catalog fact renders through load_data() from mirrored registry JSON.

For data written through the host-services capability, the namespace to pass is the writing plugin’s name (e.g. load_data("accent-comments", page.slug)), since that is the namespace the host derives for it.

Git Export

When git deployment is active, a background task can periodically commit _data/ snapshots to a dedicated git branch for backup and moderation.

data:
  enabled: true
  git_export:
    enabled: true
    branch: ugc          # default: "ugc"
    interval: 300        # seconds between exports, default: 300
    push: true           # push to remote after commit, default: true

Requirements:

  • data.enabled: true
  • git.enabled: true
  • --production flag (export does not run in dev mode)

The export creates an orphan branch (no shared history with the main content branch). Each cycle:

  1. Scans _data/ for .json files
  2. Compares with the previous commit’s tree
  3. Creates a new commit only if something changed
  4. Pushes to the remote (when push: true)

Push failures are logged and retried on the next interval.

Moderation Workflow

  1. UGC arrives on the publisher via plugin writes to _data/
  2. The background task commits to the ugc branch and pushes
  3. Content team reviews UGC in git (pull request or direct)
  4. Approved UGC is optionally merged into the content branch

Storage Layout

_data/
  comments/
    post-123.json
    post-456.json
  forms/
    contact-789.json

Each (namespace, key) pair maps to _data/{namespace}/{key}.json.

Edition Requirements

The data store itself is compiled into the Standard and Pro binaries. There is no build-feature gate – the store and the load_data() template function work in accent build and dev serve with no license, controlled at runtime by data.enabled. Only the git export of _data/ requires a Standard or Pro license.

ComponentLicense
Data store endpointsNone (runtime: data.enabled)
load_data() template functionNone (runtime: data.enabled)
Git exportStandard or Pro (requires git-aware)