# Berris.dev (full text) > Berris.dev is Roy Berris's knowledge base on software architecture, API design, AI agents and .NET, written from hands-on experience as a software architect. Written by Roy Berris, Software Architect at New Orange. About the author: https://berris.dev/about/. Index of all nodes: https://berris.dev/llms.txt. Nodes are listed newest first. --- # Clearing my evening thoughts with a personal VPS - URL: https://berris.dev/nodes/clearing-my-evening-thoughts-with-a-personal-vps/ - Markdown: https://berris.dev/nodes/clearing-my-evening-thoughts-with-a-personal-vps.md - Author: Roy Berris - Published: 2026-09-26 - Clusters: AI, AI Agents, Software Architecture > How I stopped evening work anxiety by wiring up a private VPS, Matrix, and an Obsidian second brain into an always-on personal AI assistant. The promise of AI-assisted engineering was supposed to be breathing room. In reality, it turned me into a high-bandwidth orchestrator. When generating implementations, reviewing distributed systems architectures, and debugging complex traces take minutes instead of hours, you don't touch one problem at a time—you touch ten. By the end of the workday, my working memory felt like a distributed system under partition: dozens of dangling references to half-reviewed PRs, edge cases in microservices, and architectural refactors still spinning in my head. Humans don't have atomic garbage collection. Without a reliable, zero-friction offload mechanism, those open threads follow you into your evening, conversations with family, and sleep. I found myself running an involuntary background process all evening, chewing mental cycles on tomorrow's problems. I needed a mental cache invalidation mechanism: a private, always-available sink where I could dump raw thoughts from anywhere, trust that they were captured and categorized, and immediately flush my working memory. Here is how I built that system on a $5 VPS using an owner-operated stack, grounded in the cognitive psychology of offloading mental residue. ## The Cognitive Science: Why Offloading Works Engineers often treat mental exhaustion as a failure of willpower or focus. In reality, it is a well-documented cognitive bottleneck. Psychological research on **cognitive offloading**—defined by Risko & Gilbert (2016) as the use of physical actions or external devices to alter the information processing requirements of a task—demonstrates that our biological working memory has strict, unforgiving bandwidth constraints. When an unresolved work problem or future intention stays in our head, the brain continues to expend executive resources rehearsing it. Furthermore, as research by Morrison & Richmond (2020) and broader studies on prospective memory show, externalizing intentions onto a dependable external store directly relieves working memory load and terminates involuntary retrieval loops. When we don't offload, we suffer from what organizational psychologists describe as **cognitive residue**: attention and working memory capacity remain tethered to an unfinished task, lingering long after we have stepped away from the keyboard and impairing our ability to engage with the present moment. Writing things down is not just about keeping a todo list—it physically and mentally frees working memory load, neutralizes lingering cognitive residue, and signals to the brain's executive control network that the loop is safely closed. The catch? If the offloading mechanism introduces friction, the brain defaults to keeping the data in working memory. ## The Architectural Requirements: Zero Friction and Absolute Privacy Commercial SaaS assistants (ChatGPT, Claude, Notion AI) are impressive, but they failed my personal requirements for two reasons: 1. **Friction kills capture**: If offloading a late-night thought requires unlocking a phone, opening a heavy app, waiting for a web view to load, and navigating menus, I won't do it. Capture must be as low-latency as sending a message to a friend. 2. **Data sovereignty**: I frequently think through proprietary architecture trade-offs, internal client constraints, and unvarnished personal thoughts. Routing those unfiltered into a multi-tenant corporate cloud felt fundamentally irresponsible. The design constraints became clear: an owner-operated stack running on a modest 2 vCPU, 4 GB RAM Ubuntu server, secured entirely behind a private mesh network, with end-to-end encrypted chat on my phone and instant synchronization into my personal markdown vault. ```mermaid flowchart TD subgraph ClientDevices ["Client Devices (MacBook & Phone)"] A["Matrix Client (Element)"] B["Obsidian App (LiveSync)"] end subgraph VPS ["Private VPS (Tailnet Secured)"] C["Matrix Server (Continuwuity)"] -->|Webhook / API| D["Hermes Agent Gateway"] D -->|Append / Query| E["Local Vault (Deno Bridge)"] E -->|Two-way Sync| F["CouchDB (LiveSync Database)"] end subgraph Offsite ["Offsite Disaster Recovery"] G["Private GitHub Repository"] end A -->|E2EE Instant Text / Voice| C B <-->|Real-time Sync| F E -.->|Hourly Git Cron| G ``` ## The Stack Breakdown The entire system runs on a cheap cloud node consuming under 450 MB of resident RAM, leaving abundant headroom. ### 1. Transport Layer: Matrix via Continuwuity Rather than building a bot on Telegram or Discord—which exposes metadata to third parties and lacks native decentralized E2EE—I deployed **Continuwuity** (a lightweight Matrix homeserver) in Docker. Using Element on my phone and laptop gives me an instant chat interface with push notifications, voice note recording, and full message persistence. ### 2. Orchestration & Agent: Hermes Gateway The agent logic runs as a managed `systemd` service (`hermes-gateway`). When a message arrives in my private Matrix control room, Hermes processes the intent: - **Raw dumps**: "Remind me to check the idempotent consumer retry loop on the order service tomorrow morning." - **Architectural sketches**: Notes on decoupling two domain boundaries. - **Vault queries**: "What did I decide last month about the JWT expiration strategy?" The gateway evaluates the message against my system prompt (`SOUL.md`) and routes it accordingly: ```yaml # hermes-agent/config.yaml snippet agent: name: "Hermes" workspace: "/var/lib/vault/second-brain" default_inbox: "inbox/daily-dumps.md" sync_strategy: "append-with-timestamp" matrix: homeserver_url: "http://127.0.0.1" listen_room: "!internal-ops:matrix.local" ``` ### 3. Second Brain Storage: Obsidian + CouchDB LiveSync My primary knowledge base is a local-first Obsidian vault. While Obsidian Sync is great for desktop-to-mobile, headless server integration requires something programmable. I run CouchDB alongside `couchdb-livesync`. A lightweight Deno bridge keeps the VPS filesystem in sync with CouchDB. When Hermes writes markdown directly to `/var/lib/vault/second-brain/inbox/`, CouchDB pushes the diff to my phone and laptop within milliseconds. ### 4. Zero Public Surface: Tailscale Overlay None of these services are exposed to the public internet: - No public domain names or DNS records pointing to the VPS. - No public reverse proxy or open HTTP/HTTPS ports. - UFW drops all inbound traffic except Tailscale's WireGuard interface (`tailscale0`) and SSH keys. Whether I am at my home desk, in the kitchen, or out running errands on mobile data, my phone connects seamlessly over the encrypted Tailnet mesh. ### 5. Disaster Recovery: Hourly Git Commits Database files can corrupt; physical nodes can disappear. An hourly cron job snapshots the vault state, agent configuration, and prompts into a private, encrypted GitHub repository: ```bash #!/usr/bin/env bash cd /var/lib/vault/second-brain && \ git add . && \ git diff-index --quiet HEAD || git commit -m "auto: vault snapshot $(date -u +'%Y-%m-%dT%H:%M:%SZ')" && \ git push origin main --quiet ``` ## The Workflow in Practice Here is what this looks like on a typical evening: 1. **The Kitchen Sanctuary**: At 18:00 PM, I arrive home carrying fresh groceries, ready to decompress. Cooking is one of my favorite hobbies—a tactile, sensory craft that usually pulls me away from terminals and monitors. But as I stand over the hot pan stir-frying chicken, watching the garlic and scallions sizzle in the oil, an unexpected work thought pops into mind: *We missed a race condition in our distributed event processor when tenant updates arrive out of order.* 2. **The 5-Second Offload**: In the past, this was where the evening dissolved into rumination. My brain would clutch the idea, turning it over in an anxious loop while dinner burned. Instead of ruminating, I reach for my phone on the counter with a clean hand and send a quick message to Hermes: *"Check the partition key on the event consumer. If two tenant updates arrive out of order, the state store could get corrupted. Put this on the todo list for tomorrow morning."* 3. **The Agent Handling**: Hermes intercepts the message over Matrix, transcribes the note, tags it `#architecture/concurrency`, and immediately appends it to my actionable todo list for the next day or upcoming week in Obsidian. 4. **The Flush**: Within three seconds, Hermes replies: *"Captured. Added to tomorrow's todo list under Architecture Review."* 5. **Taking the Edge Off**: Having a dependable place to capture the detail doesn't magically wipe my mind clean, but it takes the immediate edge off. Knowing it's safely logged stops the thought from looping in the background, making it easier to step away from work mode and get back to stir-frying my chicken. > **The Zero-Friction Rule**: If capturing a thought requires more than three taps or more than five seconds, you will hesitate. When you hesitate, you retain the thought in working memory, and working memory ruins your downtime. ## Architectural Lessons & Production Advice If you are setting up a personal assistant sink, keep these rules in mind: - **Separate Capture from Execution**: Do not ask your mobile assistant to execute complex refactors or kick off builds while you're offline. Treat it strictly as an intake and retrieval engine during off-hours. - **Local Markdown is the Ultimate Format**: Avoid proprietary databases for your notes. Plain markdown files with YAML frontmatter guarantee that you can switch tools or replace LLMs ten years from now without data loss. - **Fail Closed, Not Open**: Keep your AI assistant behind Tailscale. If a model hallucinates or an agent gateway hits an unhandled exception, it should fail quietly on an isolated loopback address, never in a way that leaks data. ## Conclusion Engineering velocity with AI is only an advantage if you have the discipline—and the infrastructure—to disconnect from it. Building a self-hosted assistant isn't a magic bullet or instant cure-all, nor is it about hoarding infrastructure or spending weekends writing YAML. It's simply a modest, practical habit backed by an owner-operated safety valve: a dependable sink that takes the edge off lingering work thoughts, keeps ideas from looping, and makes it that much easier to step away from the editor. --- # The Three-Layer System for Consistent AI Specifications - URL: https://berris.dev/nodes/the-three-layer-system-for-consistent-ai-specifications/ - Markdown: https://berris.dev/nodes/the-three-layer-system-for-consistent-ai-specifications.md - Author: Roy Berris - Published: 2026-09-26 - Updated: 2026-09-26 - Clusters: AI, Software Architecture, Best Practices > How we use a three-layer compiler pipeline to generate functional design specifications with AI, compiling business truth down to verifiable contracts. **TL;DR:** We treat the specification as the single source of truth for user intent and business rules, compiling functional design down through three strict layers. Business rules stay in native prose, the domain model structures invariants in English, and the contract or wire schema is simply the final compiled layer. Splitting the process this way stops hallucinations and eliminates schema drift. When we first asked an AI model to generate a functional specification directly from rough requirements, the result looked convincing on the surface. But when we inspected the logic, the problems jumped out. The model quietly dropped business validation rules, invented defaults out of thin air, and hallucinated system behavior. Trying to fix all of that by stuffing more instructions into one giant prompt just confused the model and made the output drift even further. People often assume specification generation is just about producing API definitions or code stubs. In our team, we look at it differently. A specification is the functional design of your system, and it is the single source of truth for user intent and business rules. Today's language models cannot bridge human intent and rigid technical contracts in one jump. To solve this, we treat functional design like a compiler chain: three distinct, strictly one-way layers that compile human truth down into a verifiable contract. ## Why Does Single-Shot Specification Lead to Drift? Most teams start by feeding user stories or meeting notes into a chat prompt and asking for an interface definition or code stubs immediately. Under the surface, the model tries to solve three completely different problems at the same time: understanding business policies, modeling domain relationships, and formatting technical contract syntax. In that single pass, it silently invents enum values, misinterprets domain invariants, and overlooks edge cases just to produce syntactically valid output. This creates silent drift between what business stakeholders expect and what engineers actually build. The trouble usually shows up late, often when validation fails in staging. When that happens, the temptation is to patch the generated schema or contract by hand. But the contract is only the final compiled artifact of your functional design. Hand-editing downstream contracts breaks the chain of truth. The next time you run an AI generation tool, it overwrites those manual fixes or diverges further because the source of truth was never updated. Splitting this workflow into sequential passes introduces a small latency trade-off. Generating intermediate representations takes a few minutes instead of a few seconds. In our experience, that extra time is worth it. You keep the context clean at each step and save yourself painful debugging sessions later. ## How Does the Three-Layer Compiler Pipeline Work? Instead of treating specification generation as a single prompt, we treat it like a compiler chain. Each stage has a single, clear job, moving strictly from human and business intent down to machine contracts. Upstream artifacts remain the single source of truth, and downstream layers are generated automatically. ```mermaid flowchart TD L1["Layer 1: Human and Business Truth
Native business rules and user intent"] --> L2["Layer 2: Functional Design
Domain model and system invariants"] L2 --> L3["Layer 3: Verifiable Contract
Compiled wire schemas and interfaces"] style L1 fill:#14432a,stroke:#4ade80,color:#dcfce7 style L2 fill:#0e3a4a,stroke:#67e8f9,color:#e0f7ff style L3 fill:#3b1f5c,stroke:#c084fc,color:#f3e8ff ``` The system organizes requirements into three distinct stages: - **Layer 1 (Human and Business Truth):** Written in native business prose to capture stakeholder rules directly. In our projects in the Netherlands, capturing policies in Dutch prevents premature translation errors and preserves regulatory nuances that non-technical domain experts care about. Stakeholders can read, verify, and own this layer directly. - **Layer 2 (Functional Design and Domain Model):** Expressed as an English architectural specification following [Domain-Driven Design](https://www.domainlanguage.com/ddd/) principles. This layer normalizes business concepts into formal entities, [value objects](/nodes/using-value-objects-in-net/), lifecycle states, and domain invariants without any transport or serialization baggage. It defines what the system does and why, independent of delivery protocols. - **Layer 3 (Verifiable Contract):** Defined in [TypeSpec](https://typespec.io/), OpenAPI, or schema definitions. This layer handles transport mechanics, status codes, query parameters, header definitions, and serialization, following the principles we described in [Designing APIs for AI Agents](/nodes/designing-apis-for-ai-agents/). Wire contracts and API schemas are not the starting point. They are the final compiled layer of functional design. This workflow is an automated compiler pipeline rather than a traditional waterfall process. We edit the upstream business rules, run the generation tool, and let the pipeline compile the downstream contracts. Nobody edits the compiled wire contract by hand. ## How Do We Capture and Validate Business Rules? You cannot expect an AI model or an engineer sitting alone to invent business truth. Layer 1 requires collaborative discovery with domain experts before any code or prompt runs. In our projects, we use three discovery techniques to draw out rules from stakeholders: - **[Event Storming](https://www.eventstorming.com/):** We gather domain experts and developers in a room to map domain events along a business timeline. We explore what happens across a process, what triggers each action, and which policies govern state changes. - **[Example Mapping](https://cucumber.io/blog/bdd/example-mapping-introduction/):** We take each user story and break it down into concrete business rules illustrated by realistic examples. Talking through concrete scenarios reveals edge cases and hidden assumptions that abstract bullet points conceal. - **Stakeholder interviews:** We talk directly with product managers, operational staff, and compliance officers. Capturing their exact words in their native language preserves legal and operational nuances that get lost when developers translate requirements straight into technical jargon. Capturing fragments of rules is only half the battle. You also need to validate the requirements as a coherent whole before feeding them to an AI compiler. We run validation sessions with the business to check completeness. We walk through the end-to-end user journey to make sure every failure mode, boundary condition, and lifecycle transition has an explicit rule. Walking through realistic examples surfaces edge cases early, when changing a rule costs nothing. Once the rules are complete and unambiguous, we secure formal sign-off from business stakeholders. Because Layer 1 uses plain language without HTTP status codes, JSON fields, or database tables, non-technical experts can read every sentence and take full ownership. This sign-off freezes the baseline for Layer 1. Only after the business approves Layer 1 does the AI compiler pipeline turn those rules into downstream models and contracts. ## Tracing an Example Through the Three Layers: A Library Book Loan To see how this works in practice, consider a classic scenario: a member borrowing a physical book from a library. Walking this feature through the three layers shows how business intent compiles into a technical contract while maintaining complete traceability. ### Layer 1: Business Rules Layer 1 captures the intent and rules in plain language approved by the library staff: - **User Intent:** A registered library member wants to borrow a physical book. - **Rule 1 (Loan limit):** A member can have at most 5 active book loans at any time. - **Rule 2 (Good standing):** A member cannot borrow books if they have an unpaid fine. - **Rule 3 (Loan period):** The standard loan period is 21 days from the date of checkout. There are no endpoints, status codes, or database keys here. Any librarian can read this list and confirm whether it accurately describes how the library operates. ### Layer 2: Domain Model Next, the compiler pipeline generates the English architectural specification. Layer 2 applies domain-driven design concepts to model entities, invariants, post-conditions, and domain events: - **Entity:** `Loan` (attributes: `loanId`, `memberId`, `bookId`, `loanDate`, `dueDate`, `status`). - **Domain Invariant (`BorrowBookPolicy`):** A loan can only be created if the member's current active loans count is less than 5, and the member's unpaid fine balance is zero. - **Post-conditions:** A new `Loan` is instantiated with `status: Active` and `dueDate` calculated as `loanDate + 21 days`. - **Domain Event:** The system emits a `BookBorrowed` event containing `loanId`, `memberId`, `bookId`, and `dueDate`. Layer 2 defines what the domain logic must enforce. It still contains zero HTTP headers or serialization details. ### Layer 3: Verifiable Contract Finally, the pipeline compiles Layer 2 into a verifiable API contract. Here is the OpenAPI definition for the borrow operation: ```yaml paths: /members/{memberId}/loans: post: summary: Borrow a book operationId: borrowBook security: - bearerAuth: ["loans:borrow"] parameters: - name: memberId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - bookId properties: bookId: type: string responses: "201": description: Book borrowed successfully content: application/json: schema: $ref: "#/components/schemas/Loan" "409": description: Business policy violation content: application/json: schema: type: object required: - error properties: error: type: string enum: - loan-limit-reached - unpaid-fine ``` Notice how this contract structures the response codes. The schema returns a 201 status code with the created loan on success, or a 409 Conflict with a machine-readable error when a domain rule fails. ### Emphasizing Traceability Look closely at how every line in this Layer 3 contract traces directly back through Layer 2 to Layer 1: - The path `/members/{memberId}/loans` and operation summary `Borrow a book` trace directly to the plain-language user intent in Layer 1. - The required permission `loans:borrow` traces to the library policy that the caller must have borrowing privileges. - The `409` error enum `loan-limit-reached` traces through the Layer 2 `BorrowBookPolicy` invariant back to Rule 1 (limit of 5 active loans). - The `409` error enum `unpaid-fine` traces through the Layer 2 invariant back to Rule 2 (no unpaid fines allowed). - The `dueDate` in the returned `Loan` object traces through the Layer 2 post-condition back to Rule 3 (21-day loan duration). Nothing in the contract appears out of thin air. If a model generates an unexpected error code or an extraneous query parameter, you can flag it instantly because it lacks an upstream parent in Layer 1. Every line in the contract traces directly back to an approved business rule. ## Why Is This Three-Layer Scaffolding Temporary? We want to be clear about why this system exists today. These three separate stages are practical scaffolding built around the limits of current language models. Right now, models cannot reliably preserve nuance across multiple levels of abstraction in a single inference pass. We think of this three-layer pipeline as a deliberate engineering hedge. It stabilizes model outputs today without locking our architecture into rigid, permanent workflow machinery. We documented this architectural boundary choice in an [Architecture Decision Record (ADR)](https://adr.github.io/) so our team understands why we enforce these boundaries and under what conditions we can simplify them. Eventually, models will be capable enough to jump from business discussions to validated wire schemas without intermediate steps. When that shift happens, explicit file-based handoffs between layers will disappear from our daily work. Even then, the underlying separation of concerns between business truth, functional design, and verifiable contracts will remain structurally sound. ## How Does the Pipeline Stay Consistent When Models Change? Model behavior changes with every new release, but this pipeline keeps our specifications stable. Layer 1 functions as source code, while Layer 2 and Layer 3 act as compiled build artifacts. When business requirements shift, we update the business rules in Layer 1 and trigger a clean compilation rather than patching downstream files. In our team, we store domain rules, conventions, and architectural constraints inside version-controlled repository instructions and skills. Keeping guidance in git repositories ensures every engineer and continuous integration agent runs the exact same prompts. Ad-hoc chat sessions lose context quickly, but versioned skills keep that knowledge in the repository where everyone can use it. The architecture remains completely tool-agnostic. You can switch the underlying foundation model or migrate from TypeSpec to another interface definition language whenever you choose. Because your core functional design lives upstream in clean domain models, changing a code generator never forces a rewrite of your business rules. When tools improve, downstream layers are simply regenerated. ## Recommendations for Your Team Setting up this pipeline requires discipline around layer boundaries and prompt management. If you want to set up this system in your own projects, here is our practical advice: - **Adopt a spec-first mindset:** Treat functional design as the single source of truth for user intent and business rules, not code stubs or handwritten schemas. - **Treat Layer 1 as the sole source of truth:** Update business rules in native prose and secure business sign-off before compiling downstream layers. - **Ensure strict traceability:** Make sure every property, operation, and error status in Layer 3 is derived from and traceable to an approved Layer 1 rule. - **View the wire contract as the compiled layer:** Treat APIs and schemas as the final compiled representation of functional design, not the starting point. - **Ground Layer 2 in domain-driven design:** Express domain invariants, entities, and events in clean English before worrying about transport protocols. - **Regenerate downstream layers when tools improve:** Treat Layer 2 and Layer 3 as build artifacts. When you upgrade models or linters, recompile from Layer 1 instead of hand-patching files. - **Automate downstream compilation:** Run models in strict one-way passes using automated scripts or continuous integration tasks. - **Version control your prompt instructions:** Commit architectural rules, ADRs, and skills to your git repository alongside the project code. - **Validate contracts with deterministic tools:** Use standard TypeSpec compilers or schema linters to catch syntax errors and contract flaws immediately. - **Never edit generated layers by hand:** If you hand-tweak Layer 2 or Layer 3 outputs, subsequent compilations will wipe out your modifications. - **Avoid two-way sync:** Never attempt to back-propagate changes from a wire contract back into the domain model. Keep it strictly one-way. - **Keep transport details out of business rules:** Serialization quirks, status codes, and HTTP headers do not belong in Layer 1 or Layer 2. ## Conclusion A specification is not just an API contract. It is the functional design that captures user intent and business rules as your single source of truth. By treating functional design as a three-layer compiler pipeline, you stop hallucinations and keep your contracts consistent with what the business actually needs. Give the model one job at a time, and let the compiler do the rest. ## FAQ ### Is this three-layer system only for APIs? No. While the final layer often produces API schemas or interface definitions, the pipeline is about generating the functional design as a whole. The specification is the single source of truth for user intent and business rules, and the contract is simply the final compiled layer of that functional design. ### How do you capture business rules before compiling? We capture business rules through collaborative workshops with domain experts, using Event Storming to discover events, Example Mapping to pin down rules and edge cases, and stakeholder interviews. We validate the rules as a coherent whole and secure formal business sign-off before running the AI compiler pipeline. ### Why write Layer 1 in native language instead of English? Writing Layer 1 in native business prose, such as Dutch in our projects in the Netherlands, captures policies directly from stakeholders without premature translation. This preserves regulatory nuances and policy details that domain experts care about, before Layer 2 translates and normalizes them into English domain concepts. ### What happens when business requirements change or tools improve? You update the approved business rules in Layer 1 and recompile downstream layers through the automated pipeline. Because Layer 2 and Layer 3 are compiled build artifacts, you never hand-patch downstream files, and you can regenerate your entire contract whenever your foundation models or toolchain improve. --- # Designing APIs for AI Agents: Schemas, Security and MCP - URL: https://berris.dev/nodes/designing-apis-for-ai-agents/ - Markdown: https://berris.dev/nodes/designing-apis-for-ai-agents.md - Author: Roy Berris - Published: 2025-10-21 - Updated: 2026-09-26 - Clusters: API Design, AI Agents, Software Architecture, Design Patterns > AI agents now call our APIs, but few of us design for them. How I use schemas, consistent patterns, security and MCP ideas to serve developers and agents. **TL;DR:** [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/) shows that 89% of developers use AI in their daily work, but only 24% design APIs with AI agents in mind. My answer is to design for humans and AI agents at the same time: treat the schema as the shared language, put business context in it, keep every pattern consistent, rethink security for automated consumers and design endpoints as tools with clear contracts. The API development world changed a lot in 2024, and it caught many of us by surprise. While we were busy making APIs better for human developers, a new consumer appeared that works very fast: AI agents. [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/) shows clearly that 89% of developers now use AI tools every day, but only 24% design APIs with AI agents in mind. This gap shows a big problem that needs new design patterns. ## Why Do AI Agents Need Different API Design? When I design APIs today, I still think about the developer who will read my docs, understand my endpoint patterns, and write code to connect with it. But here's what Postman's research showed that really changed how I think about building APIs: AI agents are already using APIs at huge scale with a 40% increase from last year. The disconnect is there. While 89% of developers use AI tools for making code and solving problems, most of us keep designing APIs using patterns made for human use. Only 13% design equally for humans and AI agents, while just 7% mainly design for AI agents. This mismatch creates basic problems when AI agents meet APIs that don't have clear schemas, typed errors, and clear behavioral rules. ## How Do You Design an API for Both Humans and AI Agents? The key thing I've learned is that AI agents are trained on human language, which means we shouldn't design only for machines. Instead, we need to design for both humans and machines at the same time through consistent, well-documented interfaces that show intent and purpose. ### Schema as the Shared Language I think of the API schema as the shared language between humans and machines. The schema is not just a technical contract. It's a complete way to communicate that shows intent, purpose, and business context in ways both developers and AI agents can understand. **Semantic Metadata: Intent and Purpose** The schema should tell a story about what the API does and why it exists. This means using clear field names that show business concepts, useful error codes that help fix problems, and response structures that show how things work together. When an AI agent sees a well-designed schema, it understands not just what data to send, but why that data matters and how it connects to real business work. **Functional Documentation: Business Context** Traditional [OpenAPI](https://spec.openapis.org/oas/latest.html) specs focus on technical contracts, but AI agents need business context to make smart decisions. I learned this the hard way when building APIs that agents couldn't use effectively. Now I put functional context directly into schema definitions through custom properties ([OpenAPI specification extensions](https://spec.openapis.org/oas/latest.html#specification-extensions), the `x-` fields) that give semantic meaning, links that connect technical operations to business outcomes, and workflow descriptions that show how endpoints work together to solve real problems. This approach changes the schema from a purely technical thing into a complete communication tool. Instead of keeping separate technical and functional documentation, the schema becomes one source of truth that shows both implementation details and business intent. **Navigation Metadata: Easy Discovery** The schema should help find related functionality through structured navigation data. This includes relationship patterns that mirror real-world workflows, endpoint hierarchies organized around functional use cases rather than technical resource structures, and built-in guidance that helps both humans and AI agents understand when and how to use specific operations. In my experience, organizing APIs around the problems they solve, rather than just data model relationships, makes them much easier for human developers while giving AI agents clear functional context about usage patterns. **Example: Traditional vs. Agent-Aware Schema Design** Here's how the same endpoint looks when designed traditionally versus with AI agents in mind: ```yaml # Traditional approach - technical focus /api/users/{id}: get: responses: 200: content: application/json: schema: properties: id: { type: string } name: { type: string } email: { type: string } # Agent-aware approach - semantic focus /api/users/{userId}: get: summary: "Retrieve user profile for account management" x-business-context: purpose: "Account management and user support" workflows: ["user-lookup", "billing-inquiry"] parameters: - name: userId schema: type: string pattern: "^usr_[a-zA-Z0-9]{16}$" x-semantic-meaning: "Primary user identifier" responses: 200: content: application/json: schema: properties: userId: { type: string } profile: type: object x-business-purpose: "Identity verification" accountStatus: type: string enum: ["active", "suspended", "pending"] x-business-impact: "Determines available actions" 404: description: "User not found" x-remediation: "Verify user ID format" ``` ### Consistency as a Design Language Consistency becomes the design language that both humans and machines can learn and use. This means using the same naming rules across all endpoints, standard HTTP status code usage, the same authentication methods, and error handling patterns that create a predictable interaction model. I use a pattern where every endpoint follows the same basic template: consistent parameter naming that shows business concepts, standard response envelopes that tell a complete story, the same error response formats that give useful guidance, and predictable resource relationship patterns that mirror real-world workflows. This consistency allows both human developers and AI agents to learn patterns once and apply them across the entire API surface. **Unified Communication Architecture** The diagram below shows how schema-driven design creates a unified communication layer that serves both human developers and AI agents: ```mermaid graph TB subgraph "API Schema Layer" Schema[API Schema
OpenAPI + Extensions] Schema --> SM[Semantic Metadata
Business Intent] Schema --> FD[Functional Documentation
Use Case Context] Schema --> NM[Navigation Metadata
Workflow Relationships] end subgraph "Human Consumers" Dev[Developer] DevTools[Development Tools
Postman, Insomnia] Docs[API Documentation
Swagger UI, Redoc] end subgraph "AI Consumers" Agent[AI Agent] LLM[Language Model
GPT, Claude] AutoTools[Automation Tools
Zapier, MCP Clients] end subgraph "Shared Understanding" Patterns[Consistent Patterns
Naming, Errors, Auth] Context[Business Context
Purpose, Workflows] Discovery[Discoverability
Related Operations] end Schema --> Patterns Schema --> Context Schema --> Discovery Patterns --> Dev Patterns --> Agent Context --> Dev Context --> Agent Discovery --> Dev Discovery --> Agent Dev --> DevTools Dev --> Docs Agent --> LLM Agent --> AutoTools style Schema fill:#0e3a4a,stroke:#67e8f9,color:#e0f7ff style Patterns fill:#3b1f5c,stroke:#c084fc,color:#f3e8ff style Context fill:#14432a,stroke:#4ade80,color:#dcfce7 style Discovery fill:#4a2e0e,stroke:#fb923c,color:#ffedd5 ``` ## How Should API Security Change for AI Agents? The security implications of AI agents as API consumers present a big challenge, but one that can be addressed with thoughtful design patterns. Postman's research shows that 51% of developers now cite unauthorized agent access as their top security concern, highlighting the need for better security approaches. Traditional API security designs assumed predictable human behavior, developers making dozens of calls per day, following documented patterns, operating within reasonable rate limits. AI agents challenge these assumptions by operating at high speeds, keeping persistent automated access, and potentially turning a single compromised API key into a gateway for extensive data extraction. However, these challenges create opportunities to build stronger security designs. ### Behavioral Security Architecture The unpredictable behavior of AI agents makes it hard to tell legitimate automation from attacks using traditional rule-based approaches. While I haven't needed to implement these patterns at scale yet, my team is exploring security designs that move beyond static rules to behavioral analysis systems that can recognize patterns in real-time and adjust responses dynamically. This approach needs dynamic rate limiting based on behavioral patterns, better monitoring for suspicious activity, and shorter-lived credentials with automatic rotation. The key is building systems that can tell the difference between legitimate automation and potential attacks through behavior rather than static rules. ## What Does the Model Context Protocol (MCP) Change? The emergence of the [Model Context Protocol](https://modelcontextprotocol.io/) represents an interesting development in API architecture for AI use. While 70% of developers know about MCP according to Postman's research, only 10% use it regularly. This points to growing interest but limited readiness in the ecosystem. MCP introduces new patterns for structured interfaces between AI models and real-world systems. It addresses critical problems like unified agent access, standard security models, and structured tool definitions that agents can reliably understand. The protocol defines clear boundaries between what AI agents can discover, understand, and invoke. ### Key Principles from MCP Regardless of whether MCP becomes the standard, the principles it embodies represent the direction we need to move. Structured interfaces with explicit capability declarations, clear tool definitions with typed parameters and responses, standard security models that work across different agent implementations, and discovery mechanisms that allow agents to understand available functionality. In my projects, implementing these principles improves API architecture even without full MCP adoption. Making APIs agent-consumable through structured interfaces provides immediate benefits and future flexibility, whether MCP succeeds or alternative standards emerge. **Example: Semantic Extensions in Practice** Here's how I extend OpenAPI schemas with semantic metadata in my projects: ```json { "paths": { "/customers/{customerId}/support-tickets": { "post": { "summary": "Create customer support ticket", "x-business-context": { "purpose": "Enable customers to report issues", "workflow-stage": "issue-reporting", "related-operations": [ { "operation": "GET /customers/{customerId}/support-tickets", "relationship": "list-related" } ] }, "requestBody": { "content": { "application/json": { "schema": { "properties": { "subject": { "type": "string", "x-semantic-purpose": "Primary classification for routing" }, "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"], "x-business-rules": { "critical": "Service outage affecting multiple customers", "high": "Feature broken for paying customer" } } } } } } }, "responses": { "201": { "x-business-outcome": "Customer issue is now tracked", "content": { "application/json": { "schema": { "properties": { "ticketId": { "type": "string", "x-semantic-purpose": "Primary reference for follow-up" } } } } } } } } } } } ``` This shows how semantic extensions (`x-business-context`, `x-semantic-purpose`) provide context for both developers and AI agents. ### Preparing for Protocol Evolution The low adoption rate suggests practical barriers that the industry needs to address, but the design patterns remain valuable. Building APIs with explicit capability declarations, structured tool definitions, and clear security boundaries positions systems to adapt to whatever standards emerge. This approach means designing APIs as tool catalogs rather than simple data interfaces. Each endpoint becomes a tool with clear inputs, outputs, and behavioral contracts that both human developers and AI agents can understand and invoke reliably. ## Key Design Priorities for the Future Based on what I've seen in recent projects and the trends Postman identified, there are big changes coming in API architecture. The shift from human-focused to machine-focused design needs new patterns, different security models, and better documentation strategies. Three design priorities emerge as critical for systems that need to support both human developers and AI agents well. ### Machine-First Interface Design APIs must be built with AI agents as primary users rather than afterthoughts. This means designing interfaces that provide machine readable schemas, predictable patterns, and comprehensive behavioral specifications from the ground up. The architecture should assume automated use and optimize for reliability at high speeds. ### Adaptive Security Architecture Security models must evolve beyond traditional patterns to handle high-speed exploitation, persistent automated attacks, and unpredictable behavior. This needs architectural approaches that can tell the difference between legitimate automation and attacks through behavioral analysis rather than static rules. ### Tool-Oriented Design Patterns API architecture must shift from simple data interfaces to structured tool catalogs where each endpoint represents a well-defined capability with clear inputs, outputs, and behavioral contracts. This design pattern enables both human developers and AI agents to understand and invoke functionality reliably. ## Conclusion The API landscape stands at a turning point where we must design for both human understanding and machine use at the same time. The key insight is that AI agents are trained on human language patterns, which creates an opportunity to build unified architectures that serve both audiences well. My experience shows that the practices needed for effective human-machine API use (consistent patterns, schema-driven functional documentation, and discoverable use case context) create better APIs overall. These aren't competing design goals but approaches that work together and strengthen each other. The future belongs to APIs that work as complete communication systems, where schemas serve as natural language interfaces and functional documentation is discoverable through the same mechanisms that AI agents use for technical discovery. The design patterns we implement today will determine whether our systems can communicate effectively with both human developers and AI agents. The time to start building these unified communication architectures is now. ## FAQ ### How many developers design APIs for AI agents? According to [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/), 24% of developers design APIs with AI agents in mind. Only 13% design equally for humans and AI agents, and 7% mainly design for AI agents. At the same time, 89% of developers use AI in their daily work. ### What should an API schema include for AI agents? Clear field names that match business concepts, useful error codes, and the business context behind each operation. I add that context with custom `x-` extensions such as `x-business-context` and `x-semantic-purpose`, including the workflow an endpoint belongs to and which operations relate to it. ### Do I need MCP to make my API ready for AI agents? No. MCP adoption is still low, but its principles already help: explicit capability declarations, typed tool definitions, standard security models and discovery. In my projects, applying those principles improves the API even without full MCP adoption. ### How do AI agents change API security? AI agents work at high speed with persistent automated access, so a single compromised API key can open the door to large-scale data extraction. My team is exploring behavioral analysis, dynamic rate limiting and shorter-lived credentials with automatic rotation instead of static rules. I haven't needed these patterns at scale yet. --- # AI-Assisted Blogging: When Technology Meets Technical Writing - URL: https://berris.dev/nodes/ai-assisted-blogging/ - Markdown: https://berris.dev/nodes/ai-assisted-blogging.md - Author: Roy Berris - Published: 2025-09-29 - Updated: 2026-09-26 - Clusters: AI, Blogging, Technical Writing > How I blog with AI as a writing partner, not a ghostwriter: I bring the insights and own every technical claim, AI helps with structure and readability. **TL;DR:** I use AI as a writing partner, not a ghostwriter. I give it my outline, rough notes and project material, it writes a structured first draft, and then I rewrite section by section until every technical detail is accurate and it sounds like me. The ideas and technical claims are always mine; AI helps with structure and readability. I'll be honest with you, I'm fundamentally lazy when it comes to certain aspects of writing. Not the thinking part, mind you. I love diving deep into technical problems, architecting solutions, and sharing insights from real-world implementations. But when it comes to polishing my thoughts into coherent, well-structured prose that doesn't make readers want to close their browsers immediately? Well, let's just say I'd rather delegate that task to someone (or something) more capable. Enter AI-assisted blogging: my new favorite productivity hack. ## The Problem: Technical Expertise vs. Communication Skills As software architects, we face a persistent gap between having valuable technical insights and presenting them effectively. My experience spans numerous .NET implementations and architectural decisions, but transforming these into compelling blog content requires different skills entirely. Traditional technical writing takes a lot of time for structuring, refinement, and making things readable. This overhead makes blogging feel impossible when you're already managing demanding project schedules. ## How Do I Use AI Without Losing My Voice? I think of AI as my writing partner rather than my ghostwriter. I dump my technical insights, experiences, and half-formed ideas into rough outlines or brain dumps. Sometimes it's just bullet points about a particularly gnarly architecture decision I made, or notes about why a certain implementation approach worked better than expected. AI takes this raw material and helps me structure it into something readable. It's not generating the ideas or making technical claims, that's all me. Instead, it's figuring out how to organize my thoughts coherently, smoothing out my awkward phrasing, and making sure the whole thing flows without putting readers to sleep. It also helps me identify when I should explain common design patterns or architectural concepts that you might not be familiar with, suggesting where additional context would be helpful. I maintain complete editorial control throughout the process. Every technical insight gets my stamp of approval, and if AI suggests something that doesn't align with my actual experience or opinion, it gets scrapped. The goal is amplifying my voice, not replacing it with some generic technical writing template. ## What Does My AI Blogging Workflow Look Like? Here's how this actually works in practice. I start by outlining the subject and identifying the key points I want to cover. Then I gather all my supporting materials, emails from project discussions, PDF documentation, [Lucid](https://lucid.co/) diagrams I've created, [LikeC4](https://likec4.dev/) architectural models, meeting notes, whatever artifacts capture the real story behind the technical decisions. I dump all of this into AI along with my outline and let it write the first draft of the blog post. This gives me a structured starting point rather than staring at a blank page. But here's where I become the orchestrator, I go through section by section, moving content around, rewriting entire paragraphs, adding my own examples, and making sure the technical details are accurate and reflect my actual experience. I'm the final editor of every post, even with AI as my writing partner. AI might suggest a flow or help with transitions, but I'm the one deciding what stays, what goes, and what needs to be completely rewritten to sound like me rather than a polished corporate blog written by ChatGPT. ## What Have I Learned from Blogging with AI? This collaboration has some benefits beyond just saving time. Working with AI has actually improved my own writing skills because I see how it structures sentences and organizes ideas. I've also learned that the more specific I am about who's reading the blog and how technical to get, the better the AI's output becomes. This approach has turned blogging from something I rarely did into something I might do regularly. ## Recommendations for You If you're thinking about trying AI-assisted blogging, start with clear rules about who owns what. Keep complete control over technical accuracy and your professional opinions, use AI specifically to help with communication. Set up consistent review processes and document how you work with AI so you get reliable results. Think of AI as something that multiplies your communication abilities without replacing your expertise. When writing becomes less of a chore, you can share knowledge more often, which benefits both your personal brand and helps other developers learn from your experience. ## Conclusion AI-assisted blogging offers a practical solution if you have valuable technical insights but struggle with communication barriers. By establishing clear boundaries, I provide the technical expertise, AI enhances the communication, and together we can create content while maintaining authenticity and technical depth. The result is more frequent, higher-quality content that amplifies rather than replaces your professional voice. ## FAQ ### Does AI write the posts on Berris.dev? AI writes the first draft from my outline and material, but it doesn't generate the ideas or make the technical claims. I go through every section, rewrite paragraphs, add my own examples and scrap anything that doesn't match my actual experience or opinion. ### What do I give the AI before it writes a draft? An outline with the key points, plus the material that captures the real story: emails from project discussions, PDF documentation, Lucid diagrams, LikeC4 architectural models and meeting notes. ### How do I keep AI-assisted posts technically accurate? I keep complete control over technical accuracy and my professional opinions. I'm the final editor of every post, and every technical insight needs my approval before it stays in. ### How do I get better output from AI when writing? Be specific about who's reading the blog and how technical to get. In my experience, the more specific I am about that, the better the output becomes. --- *This blog post was created using the AI-assisted approach described within its content. All technical insights and recommendations reflect my direct experience as a software architect, while AI helped refine the structure and readability.* --- # Standardizing API Conventions with ADRs - URL: https://berris.dev/nodes/standardizing-api-conventions/ - Markdown: https://berris.dev/nodes/standardizing-api-conventions.md - Author: Roy Berris - Published: 2025-09-05 - Updated: 2026-09-26 - Clusters: API, Software Architecture, ADR, Team Collaboration > How one two-hour DevAlign session and a set of Architecture Decision Records (ADRs) gave my team consistent API naming, versioning, errors and pagination. **TL;DR:** Inconsistent API design across a growing team led to integration errors, messy error handling and unexpected breaking changes for partners. I fixed it with a two-hour "DevAlign" session with one person each from development, testing, product management and operations, and documented every agreed convention as an Architecture Decision Record (ADR): plural resource names, URI-based versioning, one error format (code, message, details) and cursor-based pagination. Throughout my experience as a software architect, few challenges have proven as persistent and impactful as maintaining consistency across API design within growing development teams. The absence of standardized conventions creates a cascading effect of integration complexity, developer confusion, and technical debt that compounds over time. Recognizing this critical gap in our organizational practices, I initiated a systematic approach to establish unified API conventions through collaborative decision-making and structured documentation. ## What does inconsistent API design cost a team? The symptoms of inconsistent API design manifest across multiple dimensions of software development. In our organization, endpoint naming conventions varied significantly between developers, with some employing singular resource names while others utilized plural forms. This inconsistency created unnecessary cognitive overhead during integration work and increased the likelihood of implementation errors. Error handling presented another significant challenge. Different services returned error responses in disparate formats, complicating client-side error processing and making automated testing more complex. The lack of standardized pagination strategies resulted in performance issues and inconsistent user experiences across services consuming shared data sources. External partners consuming our APIs expressed frustration with unpredictable versioning schemes that led to unexpected breaking changes. These real-world consequences demonstrated the urgent need for comprehensive, well-documented API conventions to enhance system maintainability and developer productivity. ## How did ADRs and a DevAlign session fix it? I implemented a two-pronged approach centered on [Architecture Decision Records (ADRs)](https://adr.github.io/) and facilitated cross-functional collaboration. ADRs provide a structured framework for documenting architectural decisions, capturing context, alternatives considered, and rationale behind chosen approaches. This methodology ensures transparency in decision-making while creating institutional knowledge that persists beyond individual team member tenure. To operationalize this approach, I designed a collaborative workshop format I termed a "DevAlign" session. This structured engagement involved one representative from each functional role (development, testing, product management, and operations), ensuring comprehensive perspective representation in our decision-making process. The two-hour session combined focused discussion with practical prototyping exercises. This hands-on approach enabled real-time validation of proposed conventions and fostered consensus through active participation rather than passive acceptance of imposed standards. ## Which API conventions did we agree on, and why? The collaborative process yielded several key conventions that we formally documented through ADRs: **Resource Naming Standards:** We standardized on plural noun usage for endpoint paths, aligning with RESTful principles and improving API predictability. This decision eliminated ambiguity while conforming to established industry patterns. **Versioning Strategy:** URI-based versioning was selected to provide explicit visibility of breaking changes to API consumers. While header-based alternatives were evaluated, URI versioning offered superior compatibility with routing infrastructure and caching mechanisms. **Error Response Structure:** We established a consistent error object format containing code, message, and details fields. This standardization simplified client-side error handling while improving system observability and debugging capabilities. **Pagination Implementation:** Cursor-based pagination was adopted to enhance performance with large datasets and prevent data consistency issues during concurrent modifications. This approach proved superior to offset-based alternatives in our high-throughput scenarios. Each decision was thoroughly documented with context, alternatives analysis, and acknowledged trade-offs, ensuring our conventions remained both practical and adaptable to future requirements. ## Practical recommendations for implementation The success of this initiative depended on several critical factors that other organizations can leverage: **Stakeholder Engagement:** Involving representatives from all affected roles ensures comprehensive perspective consideration and builds ownership commitment to established conventions. **Documentation Rigor:** Implementing ADRs as living documents creates accountability and provides historical context for future decision-making processes. **Incremental Approach:** Beginning with core conventions and expanding systematically prevents overwhelming teams while establishing momentum for broader adoption. **Governance Framework:** Establishing periodic review mechanisms ensures conventions evolve appropriately with changing technological and business contexts. ## Conclusion: organizational impact and future considerations The implementation of standardized API conventions through structured decision-making has generated measurable improvements across our development lifecycle. Developer onboarding efficiency has increased significantly due to reduced learning overhead. Documentation quality has improved substantially, with ADRs serving as authoritative references complementing API specifications. Integration defects related to API inconsistencies have decreased markedly, contributing to enhanced system stability and customer satisfaction. External partner collaboration has become more streamlined due to predictable, well-documented interfaces. This systematic approach to convention establishment demonstrates the value of combining structured decision documentation with inclusive collaboration processes. The investment in establishing clear guidelines has positioned our organization to scale development capabilities while maintaining high standards of technical quality and developer experience. ## FAQ ### What is a DevAlign session? It's the workshop format I designed to agree on API conventions: a two-hour session with one representative from each role (development, testing, product management and operations). It combines focused discussion with hands-on prototyping, so proposed conventions get tested on the spot instead of being imposed. ### Why document API conventions as ADRs? An ADR captures the context, the alternatives considered and the reasoning behind a decision. That keeps decisions transparent and keeps the knowledge in the team when people leave. Our ADRs now serve as the reference next to the API specifications. ### Why URI-based versioning instead of header-based versioning? URI-based versioning makes breaking changes explicitly visible to API consumers. We evaluated header-based versioning, but URI versioning worked better with our routing infrastructure and caching. ### Why cursor-based pagination instead of offset-based pagination? Cursor-based pagination performs better with large datasets and prevents data consistency issues when records change during paging. In our high-throughput scenarios it worked better than offset-based pagination. --- # The Statically Generated Umbraco Website - URL: https://berris.dev/nodes/the-statically-generated-umbraco-website/ - Markdown: https://berris.dev/nodes/the-statically-generated-umbraco-website.md - Author: Roy Berris - Published: 2023-09-08 - Clusters: Umbraco, Hosting > How I turned my Umbraco website into a static site with xStatic and free Netlify hosting in under an hour, and who should and shouldn't use this approach. *Originally published on the old Berris.dev blog in September 2023 and moved here with its original text. The screenshots from the original post didn't survive the move.* Statically generating websites has been a trend for a while but is often overlooked as an strategy to avoid costs. In this blog post I will explain how I have converted this very website in to a statically generated one, making my hosting free. And it only took me under an hour. ## The Costs of Hosting Umbraco We might've all been creating Umbraco websites since the dawn of time. A good old server-side rendered, Razor website. This is probably the easiest way to start developing an Umbraco website. And actually, that is exactly what this website is. A big good chunk of code. Traditionally this website would have been hosted on an IIS Server with Umbraco 8 and lower. Which means you would have had to rent a Windows VPS. You would have to do server maintenance to make sure your server is up-to-date. This costs a lot of time, and comes with high subscription costs. Previously I was paying 10 euro's a month for a VPS (which is not too bad) but 18 euro's a month for a Windows license. That comes to a total of 38 euro's per month. That is at least a couple of night of diner. With the modern Umbraco we have access to .NET 6, which means we could run our Umbraco website on a Linux VPS. Decreasing the costs. But this requires some serious knowledge of Linux and hosting websites 'manually'. Something I don't have... But we can make it even cheaper! ## Introducing Netlify I've heard people talking about Netlify, but I never understood the power. But not anymore, let me introduce you to free hosting. Well... if you have a static website. Netlify offers a starter plan which is free, it allows you to deploy a static website to their Edge. Even allowing you to bind your own domain to it. But in short; Netlify is a cloud-based platform that simplifies web development and hosting. It offers features like continuous deployment, automatic scaling, and built-in CDN, making it easy to deploy and manage websites. *~ ChatGPT* ## Static Site Generation We cannot deploy an Umbraco website to Netlify. That is not how it works. What we can do is generate an Umbraco website to a lot of html files and deploy that to Netlify. It is called static site generation. And it is great, for most people. Because it does come with some costs. It means your website is completely static, there are no more server-side calculations on request. There is no Umbraco Forms, no Umbraco Commerce. Well, even no pagination on overview pages. So how does it work? Using the Umbraco plugin xStatic we can generate each of our published nodes to an html page. Just like a running Umbraco website it will include everything you would expect. You can also include your assets like CSS and JS. And it will deploy your media as well. And what you're left with is a bunch of folders and HTML, CSS and JS files. Which is basically what your browser is receiving anyway. Why would we have to render the HTML on every request when we can do it once? ## Working with xStatic It took me under an hour to set-up xStatic, configure Netlify and have my DNS switched from the old VPS to Netlify. For documentation on how to configure xStatic it is best to look at the documentation, this blog might be outdated soon. Quick tip: if the xStatic dashboard does not show up, you need to add it to the 'Administrators' role's sections from the Users -> Groups page. When building a profile you should think about the following things: - Make sure you have selected all of your image roots - Set your media crop definitions - Do not use crop aliases from the media picker\* but directly get the crop URL with a width and height - You can add your assets recursively with `/assets/*` - Make sure your root hostname is not / but `localhost:[port]` otherwise it won't generate the right URL's And probably the best thing is that above set-up was done in under an hour. From finding the package, to serving the website in Netlify. I did not have to change a single thing except for the image cropper. *\* As of September 2023 the media picker crop alias does not yet work with xStatic, issue raised [here](https://github.com/Mulliman/xStatic-for-Umbraco/issues/25). This also means focus points are not working.* [Go to GitHub page](https://github.com/Mulliman/xStatic-for-Umbraco) ## Who should benefit? For small websites like these a statically generated website is perfect. No hosting costs, no need to think about hosting or server maintenance. It just exists. But who shouldn't use this strategy? If you want to include any of below features you should probably not consider this way of working. - You want to work with Forms - You want to make a web shop with Commerce - You want to have overview pages with filters or pagination - You want to have personalization Basically anything that requires dynamic server responses should stay away from static site generation. ## Where is Umbraco hosted? One downside of working with static site generation and absolutely no server is that you don't host your CMS. What I did is install uSync and get my media in Git. This way your whole Umbraco database is in Git. Making it great for source control and you can install your website locally on any machine\*. I can now locally develop and edit my website, which is automatically published to Netlify. Which does mean this approach is perfect for single person blog websites, but not for big content platforms. It is not possible for more than 1 person to edit at the same time. *\* this won't include the xStatic set-up step, which has to be repeated on every new device.* ## Conclusion The second I saw the plugin I was convinced. At the moment xStatic still has some bugs which you might find here or there, but it is completely functional. I can definitely recommend this way of working if you don't match the above list. Thank you to the creator of xStatic for bringing the community such a great package. --- # Using Value Objects in .NET - URL: https://berris.dev/nodes/using-value-objects-in-net/ - Markdown: https://berris.dev/nodes/using-value-objects-in-net.md - Author: Roy Berris - Published: 2023-02-08 - Clusters: .NET, C#, Design Patterns > What a DDD value object is and how to build one in C#, using a hexadecimal color code with equality by value, a guarded factory method and immutable behavior. *Originally published on the old Berris.dev blog in February 2023 and moved here with its original text.* When talking about DDD you might've heard of value objects. What are they and how can we use them? ## What is a Value Object? When we are working with a domain one might've heard the term "Value Object". But what is this exactly? In this post I will not go in the details of the "what", but straight to the "how". The Microsoft Learn page on this is probably much clearer and to the point than this post can be. But after reading this Learn article and working with value objects in my own solution it becomes clear that there are multiple use cases for this. But to summarize in my own words: a value object is an immutable, key-less object. It's a semantic bundle of fields which together present one thing. They represent a simple entity whose equality is based on value rather than identity. [Learn more](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects) ## Dissecting Microsoft's example Looking at Microsoft's example with an Address value object it's clear what it does. All fields associated with an address are bundled in to one type and, together, they represent one value. Meaning that if we would change a property in address it would not equal the original anymore. ```csharp public class Business { public string Name { get; set; } public DateTime FoundedDate { get; set; } public string AddressStreet { get; set; } public string AddressHouseNumber { get; set; } public string AddressCity { get; set; } public string AddressCountry { get; set; } } ``` If we would have a class representing a business, and it looks something like shown above, it's logical to bind these fields together in to one class called Address. And to make this work in modern C# and .NET we'll need a base class called ValueObject. So why do we need this exactly? *Implementation as shown [on Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects#value-object-implementation-in-c) below.* ```csharp public abstract class ValueObject { protected static bool EqualOperator(ValueObject left, ValueObject right) { if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) { return false; } return ReferenceEquals(left, right) || left.Equals(right); } protected static bool NotEqualOperator(ValueObject left, ValueObject right) { return !(EqualOperator(left, right)); } protected abstract IEnumerable GetEqualityComponents(); public override bool Equals(object obj) { if (obj == null || obj.GetType() != GetType()) { return false; } var other = (ValueObject)obj; return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x != null ? x.GetHashCode() : 0) .Aggregate((x, y) => x ^ y); } } ``` In this base implementation of `ValueObject` we see one main theme. This theme is equality. The main benefit in to bundling everything up in to a value object is checking for equality. The bundle of fields should represent one thing. This means that if we would copy the object and change one field, it should not equal the original. When checking for equality we are looking at the **value of the object** and not the object itself, hence the name. It's equality is based on value and not on it's identity, there is no primary key. One could say that the values together are it's identity. ## An example with Hexadecimal Color Codes A value object does not have to be a multi-field structure. It can very well be a single field value, theoretically it always is to the outside world. We do not care about the internal structure of a value object. So what do I mean by that? We'll look at the hexadecimal color code (hex continuing after this) for this example. We all know what a hex looks like; It consists of a hashtag and 6 letters or numbers. But these letters and numbers are not any letters or numbers, they are in range A-F and 0-9. But to be more precise, those 6 letters are grouped in to 3 groups of 2. Each group represents a primary color: Red, green and blue. The hexadecimal color code format used by CSS for example is only a convenience, a way of storing those 3 values. Each of these groups is an integer value ranging from 0-255. The hex `0x00` represents 0 and the hex `0xff` represents 255. So why would we even want to save this hexadecimal color code in it's string values. Why not use integers in our value object? Well we can, and probably should, but it doesn't matter. Because the moral of the story is that it doesn't matter if our value object is a single string field with a regex protecting it's structure. Or a multi-value field where we store each individual value as integer. To the outside world it's value is unique, multiple objects with the same value equal each other. And it's value for the user is still `#AA00BB`. That doesn't mean we should store it as a string. Storing it with it's integer values allows us to do smart things with it, like calculations or hue shifts. Whatever we do, it's value is it's identity in whatever form that might be. Just to understand the programming aspect of this, let's implement it in C#. ```csharp public class HexColorCode : ValueObject { private HexColorCode(int red, int green, int blue) { Red = red; Green = green; Blue = blue; } public int Red { get; } public int Green { get; } public int Blue { get; } protected override IEnumerable GetEqualityComponents() { yield return Red; yield return Green; yield return Blue; } } ``` Above value object shows us the structure of our hexadecimal color code implementation. There is a problem with this though, we can't construct it. It has a private constructor, and this is a key aspect of value objects. We do not want to construct without ensuring a correct value. Value objects should guard their own state. If their state is wrong, it is not to be trusted. Imagine if the `DateTime` class allowed us to construct it with months that do not even exist. That is not something we want, we expect the `DateTime` to be consistent and the truth. In a real application we need to store it. This is probably using EF Core, but because of the way EF Core works (which is something we won't get in to this blog post) we need a certain way of defining our value object. We need a private constructor when using a value object. But to construct it we won't be using a classic constructor but a static method that returns an instance of our value object. This static method is also the guard that will make sure our state is right. When creating this value object we'll only allow a string with a hexadecimal color code. ```csharp public static HexColorCode From(string colorCode) { if (colorCode == null) { throw new ArgumentNullException(nameof(colorCode)); } if (!colorCode.StartsWith("#")) { throw new ArgumentException("Color code must start with #", nameof(colorCode)); } if (colorCode.Length != 7) { throw new ArgumentException("Color code must be 7 characters long", nameof(colorCode)); } var red = int.Parse(colorCode.Substring(1, 2), NumberStyles.HexNumber); var green = int.Parse(colorCode.Substring(3, 2), NumberStyles.HexNumber); var blue = int.Parse(colorCode.Substring(5, 2), NumberStyles.HexNumber); return new HexColorCode(red, green, blue); } ``` Now that we can create an instance of our value object, we have no way of displaying it. But we just need to write an override for the `ToString` method, which will convert it back to it's original value. ```csharp public override string ToString() { return $"#{Red:X2}{Green:X2}{Blue:X2}"; } ``` ## Testing it Now that we have a simple value object we can use it. I recommend writing Unit Tests for every value object and test for creation and conversion. For this post we will keep it simple so I wrote a simple console app that implements above example. ```csharp var colorOne = HexColorCode.From("#AA024B"); var colorTwo = HexColorCode.From("#AA024B"); Console.WriteLine($"Color One: {colorOne} ({colorOne.Red,3}, {colorOne.Green,3}, {colorOne.Blue,3})"); Console.WriteLine($"Color Two: {colorTwo} ({colorTwo.Red,3}, {colorTwo.Green,3}, {colorTwo.Blue,3})"); Console.WriteLine($"Are they equal? {colorOne.Equals(colorTwo)}"); ``` This will now output below example. If we would change a single value it will output false. If we would give it a value that is not supported, it throws an exception. Remember that this is not comparing the two input strings but rather the value of the object which are stored as three integers. ```text Color One: #AA024B (170, 2, 75) Color Two: #AA024B (170, 2, 75) Are they equal? True ``` ## Object oriented We now have a value object in its simplest form. It is created and converted back to a string value. But we can do more with value objects, we can add methods that do something with the values of the value object. We could for example invert the colors. This is where it becomes interesting, this type is no longer stupid simple, it actually does something. Below example will invert the colors and return a new instance of the value object. Because of the immutability of value objects, we will always return a new instance instead of changing the current value. ```csharp public HexColorCode Invert() { return new HexColorCode(255 - Red, 255 - Green, 255 - Blue); } ``` If we now add this method in our example console app we can see it in action. ```csharp var colorOne = HexColorCode.From("#AA024B"); var colorTwo = colorOne.Invert(); Console.WriteLine($"Color One: {colorOne} ({colorOne.Red,3}, {colorOne.Green,3}, {colorOne.Blue,3})"); Console.WriteLine($"Color Two: {colorTwo} ({colorTwo.Red,3}, {colorTwo.Green,3}, {colorTwo.Blue,3})"); ``` ```text Color One: #AA024B (170, 2, 75) Color Two: #55FDB4 ( 85, 253, 180) ``` ## Conclusion Value objects are a strong concept in creating entities and writing smarter code. This is one of many examples for value objects. The hexadecimal color code shows the importance of a value object well. We've also looked at a domain example with value objects, they should be more than simple data-transfer object. --- # Going Headless with MVC: Umbraco Community Day Talk - URL: https://berris.dev/nodes/going-headless-with-mvc/ - Markdown: https://berris.dev/nodes/going-headless-with-mvc.md - Author: Roy Berris - Published: 2023-01-19 - Clusters: Umbraco, Software Architecture > My Umbraco Community Day 2023 talk on headless and MVC: decoupling Umbraco from the presentation layer, with the source code of the working prototype. *Originally published on the old Berris.dev blog in January 2023 and moved here with its original text.* On the 19th of January in 2023 I did a talk about Headless and MVC with Umbraco on Umbraco Community Day. ## Umbraco Community Day On the 19th of January 2023 I was fortunate enough to be able to talk about Headless and MVC to a big portion of the community. Thank you all for watching if you were there. And thanks to the organization to make it all possible. In my talk I discuss headless from a software design perspective. What we need to make something "Headless", and how we would do this with a server-side rendered application. Mainly talking about component dependencies and the importance of boundaries between components. Decoupling Umbraco to "protect" the presentation from changes. Both from self-inflicted changes, or changes from Umbraco themselves. ## The Source Code In the talk I showed snippets of a prototype working with these concepts. This prototype is actually a stripped down version of the website this post was first published on. The same concepts, but worked out further, were implemented on that blog. Of course this is super overkill for this small little website. But as an enthusiast I just had to go all out. I decided to make the source code of this prototype for everyone to see, so you can find it on my GitHub page. *At this time the project is a prototype and nowhere near completion. I'd like to continue developing this into a sort of template if there would be any interest. You can star the repo to follow changes or developments.* [Go to GitHub](https://github.com/royberris/GoingHeadlessWithMVC) --- # Start Using Roslyn Code Analyzers - URL: https://berris.dev/nodes/start-using-roslyn-code-analyzers/ - Markdown: https://berris.dev/nodes/start-using-roslyn-code-analyzers.md - Author: Roy Berris - Published: 2022-10-10 - Clusters: .NET, Best Practices, Team Collaboration > How to set up Roslyn code analyzers like StyleCop with a shared .editorconfig in .NET, configure rule severity, fix issues and suppress rules the right way. *Originally published on the old Berris.dev blog in October 2022 and moved here with its original text. The screenshots from the original post didn't survive the move.* A nice thing that Roslyn (the compiler platform for .NET) offers is static code analysis. We can use this to our advantage to keep the project maintainable and in line with our code style. ## What is static code analysis Static code analysis is running a small program over your code to give you feedback on the code you have written. When we say this is static it means it doesn't really know anything about the context of the application, it only looks at code. This can help with finding bugs, but mainly to keep one code style. The code analyzer will give you warnings about things that are not compliant to the configured code style. If you do not comply to the code style, you'll get an indicator as what is wrong and what needs to change. Within .NET we have Roslyn Analyzers. Visual Studio has one built in, but you are also able to install additional analyzers from NuGet or with Visual Studio Extensions. These analyzers focus on maintainability, readability and design. But sometimes also on security and best practices. ## Why use static code analysis When you are working with multiple developers on a team, you might've noticed that everyone writes different. It is human to have your own writing style, but that doesn't really work for programming languages. But it compiles right? While that might be enough for your own blog website, or when working with a small team. Projects that have a longevity will benefit from static code analysis. If we keep our code in one style, whomever wrote it, that will be beneficial for maintainability and readability. It's easier to onboard new colleagues because the project is in one style, and it is readable. Not every module has its own author. The goal of a style and design analyzer is to keep the code authorless. While it might be true that static code analysis is helpful, it will not cover everything. Even within the code style you can still write bad code. It isn't a magic trick to fix all problems. ## Getting started Creating a code style seems to be a big task. But it's quite easy in the context of a .NET app. I recommend that you set a team meeting. Get a project you are working on. Install a code analyzer and start configuring. You can of course also do this on your own, but when working with a team it is nice to get the input of your colleagues because it is a tool that influences everyone. The code style is configured in a `.editorconfig` file. This is a file type that is IDE independent and all major IDE's will be able to interpret it. Visual Studio (Code) works with it, so does Rider. As an example, we will use [StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers), which is an analyzer I use in all of my projects. StyleCop focusses on style and design. Installation for other analyzers is similar but their configuration might be different. Go to the NuGet Manager and install `StyleCop.Analyzers.Unstable`\*. After this open your `.csproj` file, copy the `PackageReference` in to its own `ItemGroup` and give it the label `"CodeAnalysis"`. I included an example below. More on this further on in the blog post. *\* We use StyleCop Unstable because code analysis is only for development. In StyleCops case they will not support the latest features unless you have the unstable version. I wish this was different, but it doesn't really matter for now. It's important that you are aware of this, and you should do what you prefer.* ```xml all runtime; build; native; contentfiles; analyzers; buildtransitive ``` Rebuild your project and you'll likely get a ton of warnings in the Error List tab in Visual Studio. If you don't see this window, go to View > Error List and enable it. This is because we have not configured the code style with an editor config file. On the solution level, you should add a new item and call this `.editorconfig`. In Visual Studio, you'll now find that the editor config is added as a solution item. Now we need to add it for this project. We do this by linking the editor config file, which is on the solution level. This is likely one folder up, but it might depend on your project. Add the following line to the code analysis item group, with the correct path for your solution. ```xml ``` We now have an empty editor config file and a lot of warnings. Why did we put it on the solution level? This is because it is likely that your solution consists of more than one project. We want the whole solution to have the same rules, maintaining more than one editor config is not really doable. With this trick you can have the same editor config over multiple projects. ## Configuring StyleCop If you look at your Error List, you'll see a lot of warnings. For the sake of this blog post I've created an example project with some typical problems. File names not matching their first type, single line if statements, trailing whitespaces, etc. For StyleCop, we need to do one additional step. We need to add the `stylecop.json` file to the project. Official documentation [here](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md). But in our case, we'll do it the same way as we did with the editor config file. We will add it to the solution. If you go to any warning in the project from the source code and open the quick actions tab. It will give you the option "Add the StyleCop settings file to the project". Clicking this will add a JSON file to your project. Move this JSON file to your solution folder. And include below snippet in the code analysis item group. Open the settings file and rename the placeholder company to your own company name or leave it empty. ```xml ``` Open the Error List and look at the first warning, code SA0001: "XML comment analysis is disabled due to project configuration". This refers to the CSC file, which doesn't help you any further. The solution is to enable comment analysis in the `.csproj` file by adding the line below to the upper most `PropertyGroup`. ```xml true ``` You'll notice that the warnings have a code, like the one I showed above SA0001. This code is unique for every analyzer. In StyleCop's case it always starts with SA (stands for StyleCop Analyzer). You might've seen similar warnings before which started with IDE. This is the default analyzer from your IDE. Using this code, and the editor config we can configure and suppress analyzers. In your code, from Visual Studio (as the example). Go through the warnings and check for any warning you do not want to include. In my case I don't want SA1600 to be included for this project. It is my personal blog, and I don't need documentation on every method. Don't get me wrong, this is a great analyzer for any project where you are working with a team. Go to the warning from the source code and open the quick actions tab. You'll see the "Suppress or Configure issues" options. Expand this and choose "Configure SA1600 severity" setting this to "none". This will add a new line to your editor config file configuring this rule for the whole project. ## Severity levels There are multiple severity levels which you can configure. In Visual Studio you can choose between None, Silent, Suggestion, Warning and Error. We've set the SA1600 rule to 'none', meaning it won't be used. On default all rules are set to 'Warning'. But sometimes we have such an important rule, we want to configure its severity to error, this will throw an actual error when trying to build the project. This is great when something is really important, and we need to resolve this issue before being able to run the project. For example, we have SA1649: "File name should match its first type". When a class name does not match with the file name, we'll throw an error. It's easy to do, just follow the same steps as above, instead of setting it to 'None', set it to 'Error'. ## Resolving issues Almost every issue also has a resolver built in. When you open the quick actions panel on almost every warning, you will find a solution which the IDE can apply. If it isn't there, read the message carefully and try to fix it manually. Open the quick fix panel and choose the option for resolving the issue. In the case of Visual Studio, you'll also get a preview of what will change. In our example below you can see that we should prefix local calls with `this.` and VS will show us the resolution. It even asks us if we want to fix this for the document, the project or the solution. This is very powerful and will save you lots of time. I recommend using this after settling down with the analyzers to choose. ## Configuring the solution Depending on if this is a new project or an older one, you will get a lot of issues we should resolve or configure. If you don't do that for every issue, you will start to ignore issues and it's better to not use code analysis. It might be a lot of work to do the initial review, but it is time well spent. For the example we used StyleCop, this is a great analyzer which I do recommend. But you might not, or you might want to try a different one. Choose one wisely and go with it. Also, you don't need to limit yourself with one, you can also go with multiple analyzers. StyleCop focusses on style and design which will improve maintainability and readability. But other analyzers will focus on other things. For example, the [Meziantou Analyzer](https://github.com/meziantou/Meziantou.Analyzer) introduces a lot of best practices for speed and security. Even giving benchmark results on why the recommended approach is faster and better. This is a great addition to StyleCop, and you can use these analyzers side by side. After you have settled down with an analyzer, and all the issues flow in, it is best to go in with your team and tackle the issues one by one. This can either be by resolving the issue or configuring its severity. Do not forget that you can automatically fix a violation for the whole document or solution to save you time. You will find that, doing it this way, the warnings will go down in numbers quick. Do this until the list is empty! It takes some discipline to keep this list empty. If you have pipelines (CI/CD), add checks to make sure this list is empty before completing PR's. This will force you to deal with issues instead of ignoring them. ## Suppressing issues When going through the whole solution you might find some issues that you do not want to suppress for the whole solution. But maybe only for a specific folder or class. We have multiple options for suppressions: - In Source - In Source (Attribute) - In Suppressions File Forget about the first two. While they do work, I do not recommend them. It is very ugly to have one line of code that suppresses a rule once. You should ask yourself why this issue exists in the first place. Can you write the code different or resolve it another way. If you need to suppress you should do it in a suppressions file. This is a document that lives in the root of a **project** (not the solution) which applies a suppression to one type or namespace. In the suppression you need to specify a justification, so others know why we did this. And still then, I would ask the question that, if you can avoid this any other way you should. Rules are rules, we didn't define them to then break them again when it's convenient. There are some valid exceptions to this rule. One example is generated code. We'll take Entity Framework Core as a common example. When we generate migration files the filetype never matches the filename because the filename contains a timestamp, and the type doesn't. Create a `GlobalSuppressions.cs` file at the root of your project and paste below code. ```csharp [assembly: SuppressMessage( "Design", "MA0048:File name must match type name", Scope = "namespaceanddescendants", Target = "~N:CodeAnalysis.Persistence.Migrations", Justification = "Generated file will never match type name")] ``` The `SuppressMessage` takes in 2 parameters, these will specify the analyzer we want to suppress. And we can specify additional properties. We've set the scope to a namespace and its descendants. Meaning everything in the namespace and below it will suppress. Then we set a target, starting with `~N:` meaning it is a namespace. And the namespace itself behind it. Last, we give it a justification, to make sure we know why this rule is broken. Another common example is projects using CQRS and specifically using MediatR. You will find that commands, queries and their handlers live in the same file for convenience. This is because it is better for development to keep them together. This is a good reason; the code analyzer should excel development. But sometimes rules block that. That is why you can allow multiple types in one file for a specific namespace. In this example everything below the 'Features' namespace is a command or a query with their handlers and we suppress the rule to allow for our own rule. ```csharp [assembly: SuppressMessage( "StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "MediatR Queries/Commands and their handlers live in the same file for convenience", Scope = "namespaceanddescendants", Target = "~N:CodeAnalysis.Features")] ``` ## Conclusion We learned what a code analyzer is. Why we use it. What it can do for us. How to install it and how to configure it. I hope this helps in keeping projects clean and maintainable. It takes some getting used to, but once you've grasped it you cannot do a project without it. --- # Implementing a Configurable CSP in ASP.NET Core - URL: https://berris.dev/nodes/implementing-a-configurable-csp-in-aspnet-core/ - Markdown: https://berris.dev/nodes/implementing-a-configurable-csp-in-aspnet-core.md - Author: Roy Berris - Published: 2022-09-14 - Clusters: .NET, Security > What a Content Security Policy is and how to build one from appsettings.json with ASP.NET Core middleware, including report-only mode and excluded paths. *Originally published on the old Berris.dev blog in September 2022 and moved here with its original text.* The Content Security Policy is used by the browser to see which sources of data are allowed. This is an extra security layer that you should add to your website. ## What is a CSP A CSP, short for Content Security Policy, is a layer of security you can add to your websites which will tell the browser on what sources to trust. You can configure which sources to allow in your `script` source, or in your `img` source. I can for example configure to only allow images from `google.com` or scripts from `analytics.google.com` and your own domain. And why would you want this? This is a way of mitigating XSS (cross-site scripting) attacks and exploiting the browsers trust of the content received from the server. This is especially necessary when you are working with personal data, like a log-in system. The malicious scripts might run calls in the background and try to steal data. We want to limit the scripts that can be executed to be from trusted sources. [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) ## Writing a CSP Writing a CSP is simple. We need to provide the type, for which we are providing the allowed sources, and the sources themselves. We can provide full URLs, or only their domains. It supports wildcards with which you can allow all subdomains for example. Each type is separated with a semi-colon. I'll show you an example. ```text script-src 'self' analytics.google.com; img-src 'self'; ``` This will only allow scripts from 'self', meaning the current domain, or from analytics.google.com. We also won't allow any images other than from our own domain. This seems easy, right? It is, but it can be a bit complicated once we get to a real CSP. These can get very long. It's hard to maintain those CSPs, and it's better to write some code that will write it. Let's take a look at the CSP from mozilla.org at the time of writing. ```text connect-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com region1.google-analytics.com logs.convertexperiments.com 1003350.metrics.convertexperiments.com 1003343.metrics.convertexperiments.com sentry.prod.mozaws.net o1069899.sentry.io o1069899.ingest.sentry.io https://accounts.firefox.com/ stage.cjms.nonprod.cloudops.mozgcp.net cjms.services.mozilla.com; frame-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com www.youtube-nocookie.com trackertest.org www.surveygizmo.com accounts.firefox.com accounts.firefox.com.cn www.youtube.com; script-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com 'unsafe-inline' 'unsafe-eval' www.googletagmanager.com www.google-analytics.com tagmanager.google.com www.youtube.com s.ytimg.com cdn-3.convertexperiments.com app.convert.com data.track.convertexperiments.com 1003350.track.convertexperiments.com 1003343.track.convertexperiments.com; img-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com data: mozilla.org www.googletagmanager.com www.google-analytics.com adservice.google.com adservice.google.de adservice.google.dk creativecommons.org cdn-3.convertexperiments.com logs.convertexperiments.com images.ctfassets.net ad.doubleclick.net; style-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com 'unsafe-inline' app.convert.com; child-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com www.youtube-nocookie.com trackertest.org www.surveygizmo.com accounts.firefox.com accounts.firefox.com.cn www.youtube.com; default-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com; font-src 'self' ``` Good luck maintaining that. Before I will show you a way of writing CSP's in ASP.NET Core elegantly. I'll show you on how to provide it to the browser. ## Providing a CSP There are two ways of providing the CSP to the browser. This is either by providing a meta tag with the CSP in it. Or by providing it in the response headers. My preferred way of doing it is by putting it in the response headers, this way we don't have to worry about HTML and can just write middleware that will hook into every request. Let's first look at the meta approach. ```html ``` **Tip**: Providing a `default-src` is a best practice because this is a fallback for every type if you have not provided it. This is commonly set to `'self'` to only allow the website itself. Also, this is an example of only allowing images to be served over HTTPS and we won't allow any form of `iframe`. The second method is by providing the `Content-Security-Policy` header, with the same content. ```text Content-Security-Policy: default-src 'self'; img-src cdn.mywebsite.com; frame-src 'none'; ``` ### Report Only The CSP also allows for a Report Only mode. Where it will throw errors in the console when the CSP is violated, but won't actually block it. This is useful when you just created a new CSP but are not sure if it is working. Or want your website to continue working and only log violation. You enable this by changing the key from `Content-Security-Policy` to `Content-Security-Policy-Report-Only`. This is my recommendation when you've created your CSP to see if you forgot any sources. ### Reporting Violations By providing the `report-uri` you can ask the browser to report violations to that url. There are many tools to log CSP violations to, and you can also write your own. My go to is Sentry, because we have this installed as our error logging and performance tracking tool. You can read more about it [here](https://docs.sentry.io/platforms/javascript/security-policy-reporting/). Reporting works both in the normal mode and in the report only mode. ## Returning the CSP After all this we still need to write some code to return the CSP back to the browser. We will be doing this in a configurable way, where we will be able to manage the CSP from our app settings. So let's start. First thing we need is a model from our CSP. Looking at the examples above we can see two things. A CSP row exists of a type and a list of sources. These are both of type `string`, we can use a dictionary for this. We also need to be able to configure if we want to use the report only mode or not. We'll add this to the configuration as well. ```csharp public class CspConfiguration { public bool ReportOnly { get; set; } = false; public Dictionary Policies { get; set; } = new(); } ``` This configuration will bind to the following JSON structure. Below example is a copy from the app settings of this very website. ```json "Csp": { "ReportOnly": false, "Policies": { "default-src": [ "'self'" ], "script-src": [ "'unsafe-inline'", "www.googletagmanager.com", "storage.ko-fi.com" ], "style-src": [ "'unsafe-inline'", "fonts.googleapis.com", "storage.ko-fi.com", "cdnjs.cloudflare.com" ], "font-src": [ "fonts.gstatic.com" ], "img-src": [ "storage.ko-fi.com" ], "connect-src": [ "*.google-analytics.com" ] } } ``` ### Writing our middleware The last step is to write the actual middleware. In the middleware we need to read our configuration, build the CSP, and add it to the headers. First, we need to create a class called `CspMiddleware`. We will inject the `RequestDelegate` and also `IConfiguration`, and bind it to our `CspConfiguration`. ```csharp public class CspMiddleware { private readonly RequestDelegate _next; private readonly CspConfiguration _config; public CspMiddleware(RequestDelegate next, IConfiguration config) { _next = next; _config = new CspConfiguration(); config.GetSection("Csp").Bind(_config); } } ``` Next, we need to create the method `InvokeAsync` which will run when a request is made. Here we will process our wanted behavior. I chose to first process the request, before adding the CSP header. This is to avoid unnecessary computing when the request won't be finished anyway. This can happen on any number of occasions, for example an exception occurs, or validation is not met. ```csharp public async Task InvokeAsync(HttpContext context) { // Process the request first, if it is on it's return, add CSP header await _next(context); var policies = _config.Policies; // If we have no policies defined, do not include CSP if (!policies.Any()) { return; } // Build our CSP var cspBuilder = new StringBuilder(); foreach (var policy in policies) { // If the key is empty or there are no values, continue if (string.IsNullOrEmpty(policy.Key) || !policy.Value.Any()) { continue; } // Append the policy to the total cspBuilder.Append(policy.Key); cspBuilder.Append(' '); cspBuilder.Append(string.Join(' ', policy.Value)); cspBuilder.Append(';'); } // Convert to single string var cspHeaderBody = cspBuilder.ToString(); // Set right header key depending on the report only setting var cspHeaderKey = _config.ReportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; context.Response.Headers.Add(cspHeaderKey, cspHeaderBody); } ``` Make sure you add this to your `IApplicationBuilder` in your Startup or Program (depending on the .NET version you are running). Ideally, only in a non-development environment. There should be an `env.IsDevelopment()` check in there by default. Add it in the opposite of this. For reference, below example. ```csharp public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // example on what is in there by default app.UseDeveloperExceptionPage(); } else { // this will only be added in non-development environments app.UseMiddleware(); } // ... other code already there } ``` ## Excluding requests When working with software already there you might want to exclude your CSP middleware on some occasions. For example, I work with Umbraco a lot. I don't want my CSP header added when accessing the Umbraco back-office. As a little bonus I'll show you a way to exclude the CSP from these requests. In Umbraco's case, it only operates behind the `/umbraco` path segment so it's simple to add. Another example is swagger, you might not want to enforce the CSP policy for your website on the swagger URL's. First start by adding a new line to the CSP configuration. ```csharp // appsettings.json "Csp": { "ExcludeWhenPathStartsWithSegment": [ "/umbraco" ], // ... } // CspConfiguration.cs public string[] ExcludeWhenPathStartsWithSegment { get; set; } = Array.Empty(); ``` We've added it to the configuration. Now it's just not running the middleware when the request starts with this path segment. On the top of your middleware, below the next, add this code. ```csharp // If our path starts with any of the exclude segments, do not add CSP if (_config.ExcludeWhenPathStartsWithSegment.Any(i => context.Request.Path.StartsWithSegments(i))) { return; } ``` ## Conclusion You've learned what a CSP is, and how you should apply it in an ASP.NET Core web application. Maybe learnt something about the configuration, and the way middleware works. Hope this helps, make sure to keep your users safe from malicious attacks. It's not a lot of work to implement, and now, very easy to maintain.