diff --git a/skills/firely-server-plugin/SKILL.md b/skills/firely-server-plugin/SKILL.md
new file mode 100644
index 0000000..23b6cfe
--- /dev/null
+++ b/skills/firely-server-plugin/SKILL.md
@@ -0,0 +1,667 @@
+---
+name: firely-server-plugin
+description: Firely Server plugin developer. Use when building, debugging, or reviewing a custom plugin for Firely Server (formerly Vonk): custom FHIR operations, middleware, or capability statement extensions. Covers project setup, the configuration/service/contributor pattern, IVonkContext pipeline wiring, and xUnit testing with VonkTestContext. Does NOT cover repository facades — those follow a different pattern and belong in a dedicated skill.
+---
+
+# Firely Server Plugin Skill
+
+Expert guidance for building custom plugins for [Firely Server](https://docs.fire.ly/projects/Firely-Server/en/latest/). This skill sets up projects correctly, implements the configuration/service/contributor pattern, and writes testable plugin code that integrates cleanly with the server pipeline.
+
+> **Authoritative source:** Always refer to the [Firely Server Programming API Reference](https://docs.fire.ly/projects/Firely-Server/en/latest/reference/reference.html) for interface details. Do not invent interface members, method signatures, or NuGet package names — look them up when uncertain.
+
+---
+
+## What is a Firely Server Plugin?
+
+A plugin is a .NET class library that Firely Server loads at startup. It registers services into the ASP.NET Core DI container and hooks into the request processing pipeline. Plugins can:
+
+- Add custom FHIR operations (e.g. `$my-operation`)
+- Intercept any interaction with pre- and post-handlers
+- Expose non-FHIR HTTP endpoints via raw ASP.NET Core middleware
+- Extend the server's CapabilityStatement
+
+Plugins drop into Firely Server's `plugins/` folder. No server recompilation is needed.
+
+---
+
+## Project Setup
+
+### Class library project file
+
+Target `net8.0`. Reference `Vonk.Core` — it pulls in everything needed. Pin the version that matches the Firely Server release you are targeting.
+
+```xml
+
+
+
+
+ net8.0
+ disable
+ MyCompany.Vonk.Plugin.MyOperation
+ MyCompany.Vonk.Plugin.MyOperation
+
+
+
+
+
+
+```
+
+### Shared props file (pin NuGet versions once)
+
+```xml
+
+
+
+ 6.7.1
+
+
+```
+
+### Test project file
+
+```xml
+
+
+
+
+ net8.0
+ disable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Make internals visible to the test project and Moq:
+
+```csharp
+// Properties/AssemblyInfo.cs (in the main project)
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("MyCompany.Vonk.Plugin.MyOperation.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required by Moq
+```
+
+---
+
+## Core Concepts
+
+### IVonkContext
+
+Every handler receives an `IVonkContext`. Destructure it immediately:
+
+```csharp
+var (request, args, response) = vonkContext.Parts();
+```
+
+| Part | What it gives you |
+|---|---|
+| `request` | HTTP method, path, interaction type, payload |
+| `args` | Parsed FHIR/path/query arguments |
+| `response` | HttpResult code, Payload (IResource), Outcome (OperationOutcome issues) |
+
+Always call `vonkContext.Arguments.Handled()` at the start of your handler to tell the pipeline the arguments have been consumed.
+
+### VonkInteraction
+
+Controls which HTTP interactions your handler matches:
+
+| Value | Matches |
+|---|---|
+| `instance_custom` | `GET [base]/ResourceType/id/$operation` |
+| `type_custom` | `POST [base]/ResourceType/$operation` |
+| `all_custom` | Both of the above |
+| `instance_read`, `type_search`, etc. | Standard FHIR interactions |
+
+### Pipeline order
+
+`[VonkConfiguration(order: N)]` controls load order. Lower numbers run first. Check the [available plugins list](https://docs.fire.ly/projects/Firely-Server/en/latest/) for reserved ranges. Use a value in the 4500–5000 range for typical custom operations to run after built-in infrastructure.
+
+---
+
+## Plugin Anatomy — Four Files
+
+A plugin typically consists of four files:
+
+```
+MyCompany.Vonk.Plugin.MyOperation/
+├── MyOperationConfiguration.cs ← DI + pipeline wiring
+├── MyOperationService.cs ← Main handler business logic
+├── MyOperationPreService.cs ← Pre-handler logic (optional, separate service)
+├── MyOperationConformanceContributor.cs ← CapabilityStatement entry
+└── Properties/AssemblyInfo.cs ← InternalsVisibleTo
+```
+
+---
+
+## Implementation
+
+### 1. Configuration class
+
+Registers services and wires the handler into the pipeline. Must be `public static` with a `[VonkConfiguration]` attribute.
+
+```csharp
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Vonk.Core.Context;
+using Vonk.Core.Metadata;
+using Vonk.Core.Pluggability;
+using Vonk.Core.Pluggability.ContextAware;
+
+namespace MyCompany.Vonk.Plugin.MyOperation;
+
+[VonkConfiguration(order: 4700)] // Choose an order that does not conflict with existing plugins
+public static class MyOperationConfiguration
+{
+ public static IServiceCollection ConfigureServices(IServiceCollection services)
+ {
+ services.TryAddScoped();
+ services.TryAddScoped(); // register pre-handler service separately
+ services.TryAddContextAware(
+ ServiceLifetime.Transient);
+ return services;
+ }
+
+ public static IApplicationBuilder Configure(IApplicationBuilder builder)
+ {
+ // Pre-handler: runs before the main handler (in its own dedicated service)
+ builder
+ .OnCustomInteraction(VonkInteraction.all_custom, "my-operation")
+ .PreHandleAsyncWith((svc, ctx) => svc.PrepareAsync(ctx));
+
+ // Main handler for GET on instance level
+ builder
+ .OnCustomInteraction(VonkInteraction.instance_custom, "my-operation")
+ .AndMethod("GET")
+ .HandleAsyncWith((svc, ctx) => svc.ExecuteAsync(ctx));
+
+ // Main handler for POST on type level
+ builder
+ .OnCustomInteraction(VonkInteraction.type_custom, "my-operation")
+ .AndMethod("POST")
+ .HandleAsyncWith((svc, ctx) => svc.ExecuteAsync(ctx));
+
+ return builder;
+ }
+}
+```
+
+**Restrict to specific resource types** by chaining `.AndResourceTypes(new[] { "Patient", "Observation" })` before `.HandleAsyncWith`.
+
+**Post-handlers** use `.PostHandleAsyncWith`. Register them before the main handler in the `Configure` method.
+
+### 2. Service class
+
+Contains all business logic. Inject repository interfaces and loggers through the constructor.
+
+```csharp
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Vonk.Core.Context;
+using Vonk.Core.Repository;
+using Vonk.Core.Support;
+using static Vonk.Core.Context.VonkOutcome;
+using Task = System.Threading.Tasks.Task;
+
+namespace MyCompany.Vonk.Plugin.MyOperation;
+
+internal class MyOperationService
+{
+ private readonly ISearchRepository _searchRepository;
+ private readonly ILogger _logger;
+
+ public MyOperationService(ISearchRepository searchRepository, ILogger logger)
+ {
+ Check.NotNull(searchRepository, nameof(searchRepository));
+ Check.NotNull(logger, nameof(logger));
+ _searchRepository = searchRepository;
+ _logger = logger;
+ }
+
+ public async Task ExecuteAsync(IVonkContext vonkContext)
+ {
+ var (request, args, response) = vonkContext.Parts();
+ vonkContext.Arguments.Handled();
+
+ var resourceId = args.ResourceIdArgument().ArgumentValue;
+ _logger.LogDebug("$my-operation called on {ResourceId}", resourceId);
+
+ // --- your business logic here ---
+
+ response.HttpResult = StatusCodes.Status200OK;
+ response.Outcome.AddIssue(
+ IssueSeverity.Information,
+ IssueType.Informational,
+ diagnostics: $"$my-operation executed for {resourceId}");
+ }
+
+}
+```
+
+**Key repository interfaces:**
+
+| Interface | Purpose |
+|---|---|
+| `ISearchRepository` | Read resources by key or search query |
+| `IResourceChangeRepository` | Create, update, delete resources |
+| `IStructureDefinitionSummaryProvider` | Resolve schema/profile info |
+
+**Reading a resource from the database:**
+
+```csharp
+var searchOptions = SearchOptions.Latest(vonkContext.ServerBase, VonkInteraction.instance_read)
+ .WithAuthorization(vonkContext);
+var resource = await _searchRepository.GetByKey(ResourceKey.Parse("Patient/123"), searchOptions);
+```
+
+**Constructing a FHIR resource:**
+
+When you need to build a FHIR resource from scratch (rather than reading one from the repository), add `Vonk.Fhir.R4` to the main project. It transitively brings in the Firely .NET SDK R4 model classes and adds the `ToIResource()` extension method that converts a POCO to the `IResource` type Firely Server works with.
+
+Add to the main project's `.csproj`:
+```xml
+
+```
+
+Then construct the resource and convert it:
+
+```csharp
+using Hl7.Fhir.Model; // Patient, HumanName, ResourceReference, etc.
+using Vonk.Fhir.R4; // ToIResource() extension method
+
+// ...
+
+var patient = new Patient
+{
+ Id = "example",
+ Name = [new HumanName { Family = "Smith", Given = ["John"] }],
+ BirthDateElement = new Date("1990-01-01"),
+};
+
+var resource = patient.ToIResource(); // IResource — ready to assign to response.Payload
+```
+
+Use `Vonk.Fhir.R3` or `Vonk.Fhir.R5` instead when targeting those FHIR versions. If a single plugin must support multiple versions, use C# `extern alias` to disambiguate the POCO namespaces (both are `Hl7.Fhir.Model`).
+
+**Setting a resource as the response payload:**
+
+```csharp
+response.Payload = myResource;
+response.HttpResult = StatusCodes.Status200OK;
+```
+
+**Accessing the HttpContext from a service:**
+
+Use the `HttpContext()` extension method on `IVonkContext` (namespace `Vonk.Core.Context`) when you need raw HTTP access inside a regular service handler:
+
+```csharp
+using Vonk.Core.Context;
+
+// Inside your handler method:
+var httpContext = vonkContext.HttpContext(); // returns Microsoft.AspNetCore.Http.HttpContext
+var authHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault();
+```
+
+> **Warning:** `HttpContext()` throws `InvalidOperationException` if no HTTP context exists for the current invocation — for example, when the server processes a message internally rather than from an HTTP request. Only call it when you are certain the interaction originates from an HTTP request, or guard it accordingly.
+
+**Reporting errors:**
+
+```csharp
+response.HttpResult = StatusCodes.Status400BadRequest;
+response.Outcome.AddIssue(VonkIssue.INVALID_REQUEST, "Parameter 'id' is missing.");
+return;
+```
+
+**Reading a POST body (Parameters resource):**
+
+`ToTypedElement` requires an `IStructureDefinitionSummaryProvider` to resolve schema information. Inject it alongside other dependencies:
+
+```csharp
+// In the service constructor:
+private readonly IStructureDefinitionSummaryProvider _schemaProvider;
+
+public MyOperationService(
+ ISearchRepository searchRepository,
+ IStructureDefinitionSummaryProvider schemaProvider,
+ ILogger logger)
+{
+ Check.NotNull(schemaProvider, nameof(schemaProvider));
+ _schemaProvider = schemaProvider;
+ // ...
+}
+```
+
+Then use it to parse the payload:
+
+```csharp
+if (request.GetRequiredPayload(response, out var payload))
+{
+ var parameters = payload.ToTypedElement(_schemaProvider);
+ var idParam = parameters.Select("children().where($this.name = 'id')").FirstOrDefault();
+ var id = idParam?.ChildString("value");
+}
+```
+
+`IStructureDefinitionSummaryProvider` is registered by Firely Server and resolved from DI automatically, for the FHIR version in the current request.
+
+### 3. Pre-handler service class (optional)
+
+Pre-handlers run before the main handler — useful for validation, logging, or enriching the context. Give them their own service class so they can have independent dependencies and stay focused.
+
+```csharp
+using Microsoft.Extensions.Logging;
+using Vonk.Core.Context;
+using Vonk.Core.Support;
+using Task = System.Threading.Tasks.Task;
+
+namespace MyCompany.Vonk.Plugin.MyOperation;
+
+internal class MyOperationPreService
+{
+ private readonly ILogger _logger;
+
+ public MyOperationPreService(ILogger logger)
+ {
+ Check.NotNull(logger, nameof(logger));
+ _logger = logger;
+ }
+
+ public async Task PrepareAsync(IVonkContext vonkContext)
+ {
+ _logger.LogDebug("Pre-handler for $my-operation");
+ // Validate arguments, reject early, or attach items to HttpContext
+ await Task.CompletedTask;
+ }
+}
+```
+
+### 4. Conformance contributor
+
+Makes the operation appear in the CapabilityStatement hosted at `GET [base]/metadata`. Decorated with `[ContextAware]` to scope it to specific FHIR versions.
+
+```csharp
+using Microsoft.Extensions.Options;
+using Vonk.Core.Common;
+using Vonk.Core.Context;
+using Vonk.Core.Context.Guards;
+using Vonk.Core.Metadata;
+using Vonk.Core.Model.Capability;
+using Vonk.Core.Pluggability.ContextAware;
+using Vonk.Core.Support;
+
+namespace MyCompany.Vonk.Plugin.MyOperation;
+
+[ContextAware(InformationModels = new[] { VonkConstants.Model.FhirR4 })]
+internal class MyOperationConformanceContributor : ICapabilityStatementContributor
+{
+ private const string OperationName = "my-operation";
+ private readonly SupportedInteractionOptions _supportedInteractions;
+
+ public MyOperationConformanceContributor(IOptions options)
+ {
+ Check.NotNull(options, nameof(options));
+ _supportedInteractions = options.Value;
+ }
+
+ public void ContributeToCapabilityStatement(ICapabilityStatementBuilder builder)
+ {
+ Check.NotNull(builder, nameof(builder));
+ if (_supportedInteractions.SupportsOperation(VonkInteraction.all_custom, OperationName))
+ {
+ builder.UseRestComponentEditor(rce =>
+ rce.AddOperation(OperationName, "https://example.com/fhir/OperationDefinition/my-operation"));
+ }
+ }
+}
+```
+
+Support both R3 and R4 by adding both constants to the array:
+```csharp
+[ContextAware(InformationModels = new[] { VonkConstants.Model.FhirR3, VonkConstants.Model.FhirR4 })]
+```
+
+---
+
+## Raw ASP.NET Core Middleware (Optional)
+
+Use middleware when you need direct `HttpContext` access — for example, to handle a non-FHIR content type or to inspect raw request headers before the FHIR pipeline runs.
+
+You can bridge back to the Vonk pipeline from inside middleware using `httpContext.Vonk()` (namespace `Vonk.Core.Context.Http`), which returns the `IVonkContext` for the current request. This lets you read or modify FHIR context without leaving middleware.
+
+```csharp
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Vonk.Core.Context;
+using Vonk.Core.Context.Http;
+using Vonk.Core.Support;
+
+namespace MyCompany.Vonk.Plugin.MyOperation;
+
+internal class MyRawMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public MyRawMiddleware(RequestDelegate next, ILogger logger)
+ {
+ Check.NotNull(next, nameof(next));
+ Check.NotNull(logger, nameof(logger));
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ if (httpContext.Request.ContentType?.Contains("application/x-my-format") == true)
+ {
+ httpContext.Response.StatusCode = StatusCodes.Status200OK;
+ await httpContext.Response.WriteAsync("Handled by raw middleware");
+ return;
+ }
+
+ // Access IVonkContext from HttpContext when you need both raw HTTP and FHIR context
+ var (request, args, response) = httpContext.Vonk().Parts();
+ _logger.LogDebug("Middleware sees interaction {Interaction}", request.Interaction);
+
+ await _next(httpContext);
+ }
+}
+```
+
+Register it in a separate configuration class with a lower order (so it runs before FHIR parsing):
+
+```csharp
+[VonkConfiguration(order: 1200)]
+public static class MyRawMiddlewareConfiguration
+{
+ public static IServiceCollection ConfigureServices(IServiceCollection services) => services;
+
+ public static IApplicationBuilder Configure(IApplicationBuilder builder)
+ => builder.UseMiddleware();
+}
+```
+
+---
+
+## Testing
+
+`Vonk.UnitTests.Framework` is an internal Firely package and is **not published to NuGet**. Create the two helper files below in your test project — they are small and self-contained. The originals can also be found in the [Example Plugin repository](https://github.com/FirelyTeam/Vonk.Plugin.ExampleOperation).
+
+### Test helper: VonkTestContext
+
+Create `Helpers/VonkTestContext.cs` in your test project:
+
+```csharp
+using System.Collections.Generic;
+using Vonk.Core.Common;
+using Vonk.Core.Context;
+
+namespace MyCompany.Vonk.Plugin.MyOperation.Tests.Helpers;
+
+public class VonkTestContext : VonkBaseContext
+{
+ public VonkTestContext(string informationModel = VonkConstants.Model.FhirR4) : base()
+ {
+ TestRequest = new VonkTestRequest();
+ TestArguments = new ArgumentCollection();
+ TestResponse = new VonkTestResponse();
+ InformationModel = informationModel;
+ }
+
+ public VonkTestContext(VonkInteraction interaction, string informationModel = VonkConstants.Model.FhirR4)
+ : this(informationModel)
+ {
+ TestRequest.Interaction = interaction;
+ }
+
+ public VonkTestRequest TestRequest { get { return _vonkRequest as VonkTestRequest; } set { _vonkRequest = value; } }
+ public VonkTestResponse TestResponse { get { return _vonkResponse as VonkTestResponse; } set { _vonkResponse = value; } }
+ public IArgumentCollection TestArguments { get { return _vonkArguments.Arguments; } set { _vonkArguments = new VonkTestArguments(value); } }
+}
+
+public class VonkTestRequest : IVonkRequest
+{
+ public string Path { get; set; }
+ public string Method { get; set; }
+ public string CustomOperation { get; set; }
+ public VonkInteraction Interaction { get; set; }
+ public RequestPayload Payload { get; set; }
+}
+
+public class VonkTestArguments : IVonkArguments
+{
+ private IArgumentCollection _arguments;
+ public VonkTestArguments(IArgumentCollection args) { _arguments = args; }
+ public IArgumentCollection Arguments => _arguments;
+}
+
+public class VonkTestResponse : IVonkResponse
+{
+ public Dictionary Headers { get; set; } = new();
+ public int HttpResult { get; set; }
+ public VonkOutcome Outcome { get; set; } = new VonkOutcome();
+ public IResource Payload { get; set; }
+}
+```
+
+### Test helper: LoggerUtils
+
+Create `Helpers/LoggerUtils.cs` in your test project:
+
+```csharp
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace MyCompany.Vonk.Plugin.MyOperation.Tests.Helpers;
+
+public static class LoggerUtils
+{
+ public static ILogger Logger() where T : class
+ => LoggerMock().Object;
+
+ public static Mock> LoggerMock() where T : class
+ => new Mock>();
+}
+```
+
+### Writing a test
+
+```csharp
+using FluentAssertions;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Vonk.Core.Context;
+using Vonk.Core.Repository;
+using Xunit;
+using MyCompany.Vonk.Plugin.MyOperation.Tests.Helpers;
+
+namespace MyCompany.Vonk.Plugin.MyOperation.Tests;
+
+public class MyOperationServiceTests
+{
+ private readonly MyOperationService _service;
+ private readonly Mock _searchMock = new();
+
+ public MyOperationServiceTests()
+ {
+ _service = new MyOperationService(
+ _searchMock.Object,
+ LoggerUtils.Logger());
+ }
+
+ [Fact]
+ public async Task Execute_InstanceGET_ReturnsOk()
+ {
+ var context = new VonkTestContext(VonkInteraction.instance_custom);
+ context.Arguments.AddArguments(new[]
+ {
+ new Argument(ArgumentSource.Path, ArgumentNames.resourceType, "Patient"),
+ new Argument(ArgumentSource.Path, ArgumentNames.resourceId, "p1"),
+ });
+ context.TestRequest.CustomOperation = "my-operation";
+ context.TestRequest.Method = "GET";
+
+ await _service.ExecuteAsync(context);
+
+ context.Response.HttpResult.Should().Be(StatusCodes.Status200OK);
+ }
+}
+```
+
+**Mock a repository result:**
+
+```csharp
+_searchMock
+ .Setup(r => r.GetByKey(It.Is(k => k.ResourceId == "p1"), It.IsAny()))
+ .ReturnsAsync(patient);
+```
+
+**Assert an error response:**
+
+```csharp
+context.Response.HttpResult.Should().Be(StatusCodes.Status400BadRequest);
+context.Response.Outcome.Issues.Should().ContainSingle();
+```
+
+---
+
+## Deployment
+
+1. Build in `Release` configuration: `dotnet publish -c Release`
+2. Copy the output DLL (and its dependencies not already in Firely Server) into the `plugins/` folder next to `Firely.Server.dll`.
+3. Restart Firely Server — it scans `plugins/` at startup.
+4. Verify the operation appears in `GET [base]/metadata` under `rest[0].operation`.
+
+Firely Server logs which plugins it loads at startup (look for `[VonkConfiguration]` log lines at `Information` level).
+
+---
+
+## Common Mistakes
+
+| Mistake | Fix |
+|---|---|
+| Forgetting `vonkContext.Arguments.Handled()` | The pipeline treats the request as unhandled and may return a 404 or fall through to the next handler |
+| Setting response after `return` statement without a status | Always set `response.HttpResult` before returning |
+| Registering services as `Singleton` when they depend on scoped `ISearchRepository` | Use `Scoped` lifetime for your service; `ISearchRepository` is scoped |
+| Not calling `Check.NotNull` on constructor parameters | Vonk convention; use it instead of `ArgumentNullException.ThrowIfNull` |
+| Choosing an `order` that conflicts with a built-in plugin | Check the Firely Server documentation for reserved order ranges |
+| Missing `InternalsVisibleTo` for Moq | Add `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]` in `AssemblyInfo.cs` |
+
+---
+
+## Reference
+
+- [Firely Server Programming API Reference](https://docs.fire.ly/projects/Firely-Server/en/latest/reference/reference.html)
+- [Example Plugin (GitHub)](https://github.com/FirelyTeam/Vonk.Plugin.ExampleOperation)
+- [Document Operation Plugin (GitHub)](https://github.com/FirelyTeam/Vonk.Plugin.DocumentOperation)