payload-plugins
| Type | Skill |
| Plugin | awl-web · v0.0.6 |
| Invoke | /awl-web:payload-plugins |
| Source | plugins/awl-web/skills/payload-plugins/SKILL.md |
When Claude uses it
Abschnitt betitelt „When Claude uses it“Wire AWL’s @awl/payload-* packages into a Payload CMS v3 project — pick the right package for a need (access policies, typed REST client, change requests / field review workflow, dynamic refs, email templates with placeholders, feature flags, OIDC login) and register it in payload.config.ts with the exact option shapes from the docs. Use whenever someone mentions “payload plugin”, “awl payload”, “payload access policies”, “payload oidc”, “change requests payload”, “dynamic refs”, “payload email templates”, “feature flags payload”, “@awl/payload-api”, or asks how to add SSO, role rules, or a review workflow to a Payload backend. NOT for auditing existing access rules (use payload-security-scan) and NOT for SvelteKit apps without Payload (use sveltekit-app-setup).
Trigger phrases: payload plugin · awl payload · payload access policies · payload oidc · change requests payload · dynamic refs · payload email templates · feature flags payload · @awl/payload-api
Definition
Abschnitt betitelt „Definition“Registry docs: https://npm-packages.staging.appswithlove.net/?path=/docs/<id> where <id> is the doc id in the tables below. Every package is TypeScript-only and published to the AWL GitLab npm registry, so two steps repeat for each one: the @awl registry is configured and the package is listed in transpilePackages in next.config.js.
Pick the package
Abschnitt betitelt „Pick the package“| Need | Package | Doc id | Reference |
|---|---|---|---|
Composable access functions, role matrix |
@awl/payload-access-policies |
payload-payload-access-policies--readme |
references/access-policies.md |
| Typed REST client for a frontend (SvelteKit, Svelte forms, media) | @awl/payload-api |
payload-payload-api--readme |
references/api.md |
| Propose field changes for review instead of writing live | @awl/payload-change-requests |
payload-payload-change-requests--readme |
references/change-requests.md |
| Code refers to a document or value an admin picks later | @awl/payload-dynamic-refs |
payload-payload-dynamic-refs--readme |
references/dynamic-refs.md |
Admin-authored emails with {user.name} placeholders |
@awl/payload-email-templates + @awl/lexical-placeholder |
payload-payload-email-templates--readme |
references/email-templates.md, references/lexical.md |
| Per-request feature flags, optionally managed in the admin | @awl/payload-features |
payload-payload-features--readme |
references/features.md |
| Login through Google, Azure, Keycloak; bearer tokens on the API | @awl/payload-oidc or @awl/oidc/payload |
payload-payload-oidc--readme, general-oidc--readme |
references/oidc.md |
Two OIDC packages exist in the registry with different APIs (mapUserAttributes + authStrategy versus resolveUser + optional ownSession). The docs do not say which one supersedes the other, so match what the project already depends on, and for a fresh project check the live docs before choosing.
Workflow
Abschnitt betitelt „Workflow“- Read the project. Find
payload.config.ts, the users collection,next.config.js, andpackage.json. Note the Payload major (payloadin dependencies):access-policies,change-requests, andfeaturesstate Payload v3; the others do not state a constraint. - Registry access. Confirm
.npmrcor npm config maps@awltohttps://gitlab.appswithlove.net/api/v4/projects/3919/packages/npm/with a token (personal access token locally,CI_JOB_TOKENin CI). The registry README (readme--docs) has the CI and Docker snippets. - Install and transpile. Add the package with
pnpm add, then extendtranspilePackageswith the entries the package’s doc lists (some pull in@awl/utils,@awl/request,@awl/oidc-client). - Register per package using the shapes below and the matching reference file. Keep collection configs hand-written; the packages only hand you fields, hooks, and access helpers.
- Wire the interplay (next section) before writing access rules that depend on
req.user. - Verify (last section).
Registration shapes
Abschnitt betitelt „Registration shapes“Each snippet is the minimal shape from the docs. Full option tables live in references/.
// access-policies: no config registration, import helpers in collectionsimport { or, authenticatedOnly, ownerOnly, createAccessMatrix } from '@awl/payload-access-policies'const matrix = createAccessMatrix({ isAdmin: (args) => args.req.user?.role === 'admin' })access: { read: or(authenticatedOnly, () => ({ published: true })), delete: matrix({ isAdmin: true }) }// features.ts: one factory, then features.resolve(req) / features.access('flag') anywhereexport const features = createFeaturesPlugin({ features: { betaNavigation: false }, context: (req) => ({ userRoles: req.user?.roles ?? [] }), rules: [], collection: 'feature-rules' })// payload.config.ts, only when `collection` is set:collections: [{ slug: 'feature-rules', fields: featureRuleFields() }]// dynamic-refs: your own collection built from fields()import { fields as dynamicRefFields } from '@awl/payload-dynamic-refs'collections: [{ slug: 'dynamic-refs', admin: { useAsTitle: 'name' }, fields: dynamicRefFields({ relationTo: ['tags'], names: ['AssignStudentTag.tag'] }) }]// change-requests: one factory, one hand-written collection, hooks on each gated collectionexport const changeRequests = createChangeRequests({ /* slug: 'change-requests', usersCollection: 'users' */ })// ChangeRequests.ts: { slug: changeRequests.slug, access: {...}, fields: changeRequests.fields }// Courses.ts: hooks: { beforeChange: [changeRequests.hooksFor(['fees']).beforeChange] }// email-templates: placeholder feature on the richText field, render() at send timeeditor: lexicalEditor({ features: ({ defaultFeatures }) => [...defaultFeatures, placeholderFeature()] })const html = render(template.body, { user: { name: 'Alice' } })// @awl/payload-oidcexport const oidc = setupOidc({ default: 'google', providers: [{ label: 'google', issuerUrl, clientId, clientSecret, scopes: ['openid', 'email', 'profile'], mapUserAttributes: async (r) => ({ email: r.access.payload('email') }) }] })buildConfig({ auth: { strategies: [oidc.authStrategy] }, plugins: [oidc.plugin], collections: [{ slug: 'users', auth: true, fields: [...] }] })// @awl/oidc/payload (resolveUser variant)const oidc = setupOidc({ providers: [{ label: 'keycloak', issuerUrl, clientId, clientSecret, resolveUser: async (r) => ({ email: r.claims.access?.email as string }) }] })buildConfig({ plugins: [oidc.plugin] })// payload-api: frontend client, plugins composed in dependency orderexport const payload = createPayloadAPI('/api/payload').plugin(collections).plugin(globals).plugin(uploads)Order and interplay
Abschnitt betitelt „Order and interplay“| Combination | Rule | Why |
|---|---|---|
| OIDC + access-policies | Write role rules only against fields that mapUserAttributes / resolveUser fills (role, roles). |
The strategy populates req.user from the IdP claims; a role that never gets mapped is always undefined. |
@awl/oidc/payload + ownSession |
session.plugin is listed before oidc.plugin; users collection sets disableLocalStrategy: true and strategies: [oidc.bearerStrategy, oidc.sessionStrategy]; /admin/login redirects to /api/oidc/login. |
The session has to exist before the OIDC callback runs, and the local login form has no password to check. |
| features + access-policies | Spread features.featureConditions([...]) into createAccessMatrix with the role conditions. |
One matrix answers both “is the flag on” and “which role”, first match wins. |
| change-requests + access-policies | Field update access is or(fieldMatrix({ isAdmin: true }), isChangeRequest). |
Live writes stay reserved to privileged roles; everyone else can only propose through ?changeRequest=true. |
| change-requests hooks | hooksFor() returns single functions, you place them in beforeChange / afterRead next to your own hooks. |
Nothing is wired automatically, and the order relative to your hooks is yours to decide. |
| email-templates + lexical-placeholder | placeholderFeature() from @awl/lexical-placeholder/payload, then payload generate:importmap. |
The client feature resolves through the import map; a stale map fails silently at runtime. |
| payload-api plugins | uploads after collections; form after collections and globals; preferences and me need bearerAuth or login. |
Plugins build on each other’s methods. |
| payload-api + SvelteKit OIDC | payloadProxy(backendUrl) in a catch-all +server.ts forwards the access token from locals as bearer. |
The frontend client never holds the Payload URL or token. |
| dynamic-refs + multi-tenant | Add a tenant field to your collection and pass where: { tenant: { equals: tenantId } } to resolveDynamicRef. |
name is not unique, so an unscoped lookup returns an arbitrary tenant’s row. |
Environment variables
Abschnitt betitelt „Environment variables“The packages read nothing from process.env themselves; the docs pass values in explicitly. Names used in the examples, so projects stay consistent:
| Variable | Used by |
|---|---|
PAYLOAD_PUBLIC_SERVER_URL |
serverURL in buildConfig, login link in the custom login page |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET |
@awl/payload-oidc provider clientId, clientSecret, audience |
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_KEYCLOAK_ISSUER_URL, OIDC_KEYCLOAK_CLIENT_ID, OIDC_KEYCLOAK_CLIENT_SECRET |
@awl/oidc/payload providers |
OIDC_SECRET |
CookieDataStorage secret for the SvelteKit session (frontend side) |
NODE_ENV |
Example context in @awl/payload-features |
Secrets stay out of git; the example env file lists the names with empty values.
pnpm payload generate:typessucceeds and, whenplaceholderFeature()was added,pnpm payload generate:importmapran.- The dev server boots without “module not found” for an
@awl/*package; if it fails, thetranspilePackagesentry is missing. - OIDC: open
/api/oidc/login?provider=<label>&redirect=/admin, complete the IdP flow, and confirm a user row with a filledsub(or theresolveUserfields) exists. Acurl -H "Authorization: Bearer <token>" /api/<collection>returns the same data as the admin session. - Access policies: as an anonymous client
curl /api/<collection>and as each role; compare with the intended matrix. For a full audit of the resulting rules runpayload-security-scan. - Change requests: a
PATCH /api/<collection>/<id>?changeRequest=truewith a gated field creates a row in the change-requests collection and leaves the live document untouched;changeRequests.apply(req, ...)writes it and deletes the row. - Features: a custom
/flagsendpoint returningfeatures.resolve(req)flips per role or per DB rule. - Dynamic refs:
resolveDynamicRefreturnsundefineduntil an admin picks a document, then the populated document. - Email templates:
render(body, context)replaces{user.name}and keeps unknown keys as literal{key}.
Related skills
Abschnitt betitelt „Related skills“payload-security-scan: audits the access rules after wiring.sveltekit-app-setup: SvelteKit side of@awl/oidcand@awl/sessionwithout Payload.

