28.08.2026・TechStuff
28.08.2026・TechStuff

Shopware 6 Hidden Gems #14: WriteProtected & ApiAware — field-level security without a single subscriber

Fabian Blechschmidt

A code review scenario. A plugin adds a purchasePrice-style field to a custom entity — internal margin data. The review question: „Can the Store API read this?“ Followed by: „Can the Admin API write that computed field you recalculate in a subscriber?“ The answers, in the plugin at hand, were „yes“ and „yes, and then the subscriber overwrote it, except when it didn’t.“

The usual fixes are response processors, API-aware serializers, validation subscribers. The DAL has something better: two field flags that declare read and write rules on the field definition itself. The core uses them hundreds of times. The docs mention neither beyond auto-generated reference stubs.

ApiAware — who may read the field

Rule number one, and the source of most confusion: a field without the ApiAware flag doesn’t exist for any API. Not admin, not store. That’s why every field in ProductDefinition carries at least new ApiAware().

The undocumented part is the constructor argument (Framework/DataAbstractionLayer/Field/Flag/ApiAware.php:23-32):

use Shopware\Core\Framework\Api\Context\AdminApiSource;

// visible in Admin API AND Store API
(new FloatField('price', 'price'))->addFlags(new ApiAware()),

// visible ONLY in the Admin API — the Store API never serializes it
(new FloatField('margin', 'margin'))->addFlags(new ApiAware(AdminApiSource::class)),

You pass the API sources that may see the field. No argument means both; AdminApiSource::class means admin only; SalesChannelApiSource::class means store only. SystemSource — CLI commands, indexers, internal code — always has access (isSourceAllowed(), line 47-53).

That one constructor argument replaces the entire „strip sensitive fields from Store API responses“ subscriber genre. The field isn’t filtered out of the response — it’s never serialized into it, on every route, including ones added next year. Sensitive-by-definition instead of sensitive-by-remembering.

WriteProtected — who may write it

Same idea, write direction (Field/Flag/WriteProtected.php):

use Shopware\Core\Framework\Context;

(new FloatField('score', 'score'))
    ->addFlags(new ApiAware(), new WriteProtected(Context::SYSTEM_SCOPE)),

Enforcement sits deep in the write pipeline (Write/WriteCommandExtractor.php:523-545): if a payload touches the field and the context’s scope isn’t in the allowed list, the write fails with a proper constraint violation — and a genuinely helpful message:

This field is write-protected. (Got: "crud" scope and "system" is required)

With no arguments, new WriteProtected() means nobody writes it through normal channels — the DAL only accepts the value from internal defaults. The order entity does exactly this with its money fields (OrderDefinition.php:96-101): orderDate, amountTotal, amountNet are all WriteProtected(), because they’re computed by the checkout, not submitted by clients. Anyone who has wondered why you can’t PATCH an order total via API: this flag is why, and the error message above is the one you got.

Writing protected fields from your own code

The obvious follow-up: if your indexer or import should write the field, how? By running in the right scope:

$context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($payload): void {
    $this->repository->update([$payload], $context);
});

Context::scope() temporarily switches the scope for the closure. API requests run in crud scope, so they stay locked out; your system code opts in explicitly. The rule engine does this for its payload blob (RuleDefinition.php:78-80WriteProtected(Context::SYSTEM_SCOPE), and removeFlag(ApiAware::class) on top, making it invisible and unwritable externally — both flags composing is the pattern in one line).

The recipe for your entities

For every field in a custom entity definition, answer three questions:

  1. Should any API see it? No → no ApiAware flag. (Internal blobs, denormalized helper columns.)
  2. Which API? Store customers don’t need cost prices: new ApiAware(AdminApiSource::class).
  3. Who computes it? If the answer is „my code, not the client“, add new WriteProtected(Context::SYSTEM_SCOPE) and write it inside $context->scope(...).

Three declarative decisions at definition time, and an entire class of „oops, the API leaked/overwrote it“ bugs becomes structurally impossible. It also documents intent: a colleague reading the definition sees the security model without hunting through subscribers.

TL;DR

Two undocumented DAL field flags do API security declaratively: ApiAware($source) controls which API can read a field (no flag = no API at all; SystemSource always may), WriteProtected($scope) controls who can write it, enforced in the write pipeline with a clear violation message. Write from your own code via $context->scope(Context::SYSTEM_SCOPE, fn ($c) => ...). The core’s order totals and rule payloads show the pattern — copy it for anything computed or confidential.

Next up: ChangeSet — before/after values for every write, straight from the DAL, no second query.

Found fields in your project that need these flags? I bet you will — drop me an email with the count 😀


Found in Shopware 6.7.0.0: Framework/DataAbstractionLayer/Field/Flag/ApiAware.php:23-53, Field/Flag/WriteProtected.php:15-30, enforcement in Write/WriteCommandExtractor.php:308,523-545, core examples in Checkout/Order/OrderDefinition.php:96-101 and Content/Rule/RuleDefinition.php:78-80.