Documentation

Production operations

Running in production

Defaults are tuned for a typical SaaS customer. Below are the knobs you'll touch when traffic shapes change — rate limiting, retry policy, audit logging, dependency reachability probes, IHealthCheck integration, idempotency, prompt-template overrides, and the BYOK admin surface.

Rate limiting AI calls

Both POST /richtextbox/ai and POST /richtextbox/ai/stream share a per-IP token bucket so a single runaway tab can't burn through your provider quota in seconds. Defaults: 30 calls per minute per IP.

builder.Services.AddRichTextBox(opts =>
{
    opts.AiRateLimit       = 30;                          // calls per window
    opts.AiRateLimitWindow = TimeSpan.FromMinutes(1);     // window length
});

Set AiRateLimit = 0 to disable, or swap in a Redis-backed limiter:

builder.Services.AddSingleton<IRichTextBoxAiRateLimiter, RedisAiRateLimiter>();
builder.Services.AddRichTextBox();

HTTP retry on transient provider failures

OpenAI / Anthropic / Azure OpenAI return 429 and 503 under normal load. The built-in resolvers retry these (plus 502 / 504 + connection drops) with exponential backoff and full jitter, capped at 30 seconds. The provider's Retry-After header is honoured.

services.AddRichTextBoxOpenAiResolver(opts =>
{
    opts.ApiKey         = builder.Configuration["OpenAI:ApiKey"];
    opts.MaxRetryAttempts = 2;                              // 0 to disable
    opts.RetryBaseDelay   = TimeSpan.FromMilliseconds(500);
});

Idempotency keys

Every POST /richtextbox/ai, /ai/stream, and /upload respects Idempotency-Key. Duplicate retries replay the cached body verbatim — no double charging, no orphan files. Streaming responses replay the entire SSE byte stream byte-for-byte.

builder.Services.AddRichTextBox(opts =>
{
    opts.IdempotencyTtl = TimeSpan.FromHours(1);
    opts.IdempotencyPruneInterval = TimeSpan.FromMinutes(5);
});

Multi-instance hosts: register a Redis-backed IIdempotencyStore instead of the default in-memory one. Native TTL means you can disable the periodic pruner.

Security audit logging

Every security-relevant rejection emits a structured LogWarning under category RichTextBox.Audit with a stable EventId:

EventIdNameFires when
8001LicenseInvalidAn endpoint refused a request because the .lic file was missing or invalid.
8101UploadMagicByteMismatchMagic bytes don't match the declared extension.
8102UploadRejectedByValidatorAn IUploadValidator returned Reject.
8103UploadInvalidExtensionUpload rejected for being outside the allow-list.
8104UploadOversizeExceeded MaxUploadBytes.
8201AiRequestOversizeAI body exceeded MaxRequestBytes.
8202AiRateLimitExceededCaller exceeded the per-IP AI rate limit.
8203AiResolverExceptionThe AI resolver threw; logged with correlation id.
8204AiKeyVaultMissBYOK vault returned no key.
8205AiKeyVaultHitBYOK vault returned a key (KeyId logged; secret never).
8206AiResponseFilterMatchedAn IRichTextBoxAiResponseFilter mutated the response.

Routing audit events to a separate stream

IRichTextBoxAuditSink lets compliance-regulated workloads (HIPAA / PCI / SOC2) route just audit events to a write-once destination — Splunk HEC, S3 with object-lock, an audit-only DB. Ships with JsonLinesAuditSink for offline / single-instance use:

builder.Services.AddRichTextBox();
builder.Services.AddJsonLinesAuditSink("/var/log/myapp/rtb-audit.jsonl");
Behaviour. Bounded Channel + background drainer; RecordAsync never blocks. Queue overflow drops events with a DroppedCount metric. Multiple sinks register naturally — pair JsonLines with a real-time Splunk sink for free.

Dependency reachability probes

Optional companion interfaces let any RichTextBox dependency report whether its backing service is reachable. The /richtextbox/health endpoint surfaces the result alongside license status:

InterfaceBuilt-in implementationsProbe target
IRichTextBoxAiResolverProbe OpenAI, Azure OpenAI (free); AnthropicResolverWithProbe wrapper (low-token POST) GET /v1/models / GET /openai/deployments / POST /v1/messages with max_tokens=1
IIdempotencyStoreProbe Custom Redis / SQL store (you implement) PING / SELECT 1
IRichTextBoxUploadStoreProbe Custom S3 / Azure Blob / Cloudinary store HEAD bucket / list-objects --max-items 1
builder.Services.AddRichTextBox(opts =>
{
    opts.AiResolverHealthProbeEnabled        = true;
    opts.AiResolverHealthProbeTimeout        = TimeSpan.FromSeconds(3);
    opts.AiResolverHealthProbeCacheTtl       = TimeSpan.FromSeconds(30);

    opts.IdempotencyStoreHealthProbeEnabled  = true;
    opts.UploadStoreHealthProbeEnabled       = true;
});

Cache TTL on each probe defends against load-balancer hammering — a balancer hitting /health every 5 seconds would otherwise produce 12 probes per minute even when probes cost real money.

Standard IHealthCheck integration

For hosts using Microsoft.Extensions.Diagnostics.HealthChecks, register the adapter and RichTextBox status appears alongside your other checks — no parallel monitoring path:

builder.Services.AddRichTextBox();
builder.Services.AddHealthChecks()
    .AddRichTextBox(name: "richtextbox", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" });

app.MapHealthChecks("/health");

Returns Healthy when license + all gated probes pass; Unhealthy with a description identifying the failed dependency. HealthCheckResult.Data mirrors the JSON payload of /richtextbox/health.

BYOK — per-tenant API keys

Multi-tenant SaaS hosts charge AI traffic to each tenant's own provider account via IAiKeyVault + IAiKeyVaultAdmin. The package ships an in-memory reference implementation; production deployments back onto Azure Key Vault, Redis, or a real SQL store.

See the dedicated BYOK key vault page for the full contract, admin REST endpoints, and reference implementations (Redis, Azure Key Vault, encrypted-on-disk FileBackedAiKeyVault).

Per-call cost ledger

IRichTextBoxAiCostSink emits one AiUsageRecord per AI call — provider, model, mode, tokens (input / output / total), latency, status, KeyId. Forward to your billing system for tenant chargeback:

builder.Services.AddSingleton<IRichTextBoxAiCostSink, MyBillingApiCostSink>();

For high-traffic tenants, wrap with the buffering sink so the inner sees one batched flush per interval instead of one HTTP/SQL call per AI request. If your inner sink also implements IRichTextBoxAiCostBatchSink, the wrapper routes through RecordBatchAsync — one INSERT instead of N.

builder.Services.AddBufferingAiCostSink<MyBillingApiCostSink>(
    flushInterval: TimeSpan.FromSeconds(30),
    maxBatchSize:  500);

Output filtering — PII redaction, content blocking

IRichTextBoxAiResponseFilter runs after the resolver but before the response goes to the wire. The reference RegexAiResponseFilter covers ~80% of practical PII redaction:

var emailMask = new Regex(@"[\w.+-]+@[\w-]+\.[\w.-]+");
var ssnMask   = new Regex(@"\b\d{3}-\d{2}-\d{4}\b");

builder.Services.AddSingleton<IRichTextBoxAiResponseFilter>(
    new RegexAiResponseFilter(
        (emailMask, "[email-redacted]"),
        (ssnMask,   "[ssn-redacted]")));

For streaming responses, the framework buffers tokens server-side when filters are registered (BufferStreamingForFilters = true default) so a credit-card regex spanning multiple deltas actually matches.

Prompt-template overrides

IRichTextBoxPromptTemplateProvider lets you override the per-mode system prompts the built-in resolvers send. Use the PromptTemplateProviderBase decorator base class to override only the method(s) you need:

public sealed class FormalTonePromptProvider : PromptTemplateProviderBase
{
    public override string BuildSystemPrompt(RichTextBoxAiRequest request, string? operatorSuffix)
        => base.BuildSystemPrompt(request, operatorSuffix)
         + "\n\nAlways reply in formal British English; avoid contractions.";
}

For one-off mode tweaks without a full provider, use the dictionary:

services.AddRichTextBoxOpenAiResolver(opts =>
{
    opts.ApiKey = "sk-...";
    opts.SystemPromptOverridesByMode["proofread"] =
        "You are a hyper-pedantic copy-editor for legal documents.";
});

Prompt-injection screening

IRichTextBoxAiPolicy is the gate the AI endpoints consult before invoking the resolver. The built-in DefaultPromptInjectionPolicy rejects off-the-shelf jailbreak templates ("ignore previous instructions", "you are now DAN", etc.). Layer additional policies for PII redaction or tenant-scoped mode allow-lists.

builder.Services.AddSingleton<IRichTextBoxAiPolicy, DefaultPromptInjectionPolicy>();
builder.Services.AddSingleton<IRichTextBoxAiPolicy, MyTenantPolicy>();

Distributed tracing (OpenTelemetry)

Every AI call, DOCX export, and upload emits a span through the RichTextBox.AspNetCore ActivitySource. Subscribe in your tracer-provider configuration:

builder.Services.AddOpenTelemetry().WithTracing(t => t
    .AddSource(RichTextBoxDiagnostics.SourceName)   // "RichTextBox.AspNetCore"
    .AddAspNetCoreInstrumentation()
    .AddOtlpExporter());

Startup-time options validation

RichTextBoxOptionsValidator + AiResolverOptionsValidator<T> run at the first IOptions.Value resolution — before the host accepts the first request. Bad config (forgotten /, negative byte cap, heartbeat > timeout, missing API key, missing Azure deployment) throws OptionsValidationException at startup so deployments fail loudly.

No wiring required. The validators register automatically via AddRichTextBox().

Companion docs

BYOK key vault · AI providers & streaming · Cloud upload providers · Accessibility