diff --git a/docs/Profiles/API-PROFILES-DESIGN.md b/docs/Profiles/API-PROFILES-DESIGN.md
new file mode 100644
index 000000000..3a25dcf90
--- /dev/null
+++ b/docs/Profiles/API-PROFILES-DESIGN.md
@@ -0,0 +1,3056 @@
+# API Profiles Feature Design - Ed-Fi Data Management Service
+
+## Executive Summary
+
+This document describes the architecture and implementation strategy for adapting the Ed-Fi API Profiles feature from AdminAPI-2.x to the Data Management Service (DMS). API Profiles enable fine-grained data policy enforcement on API resources, controlling which properties, references, and collections are visible or modifiable based on specific operational or regulatory scenarios.
+
+### Architectural Overview
+
+The API Profiles feature is split across two services:
+
+**Config Service** (Profile Management):
+
+- Owns profile storage in `dmscs` schema
+- Provides profile management API (`/v2/profiles/*` endpoints)
+- Handles XML import/export and JSON storage (JSONB)
+- Manages profile lifecycle (create, update, delete, export)
+
+**DMS** (Profile Enforcement):
+
+- Consumes profiles via Config API (HTTP calls)
+- Enforces profile rules in request/response pipeline
+- No direct database access to profiles
+- Validates writes and filters reads based on profile rules
+
+**Key Design Decision**: DMS fetches profiles from Config API on every request (Phase 1). Local caching in DMS is recommended for Phase 2 to improve performance.
+
+## Table of Contents
+
+1. [Problem Definition](#problem-definition)
+2. [Conceptual Design](#conceptual-design)
+3. [Integration Strategy](#integration-strategy)
+4. [Database Schema Design](#database-schema-design)
+5. [Profile Resolution Flow](#profile-resolution-flow)
+6. [Enforcement Pipeline Architecture](#enforcement-pipeline-architecture)
+7. [XML to JSON Conversion Strategy](#xml-to-json-conversion-strategy)
+8. [API Specifications](#api-specifications)
+9. [Example Profiles](#example-profiles)
+10. [Security Considerations](#security-considerations)
+11. [Testing Strategy](#testing-strategy)
+12. [Appendix A: Implementation Tickets](#appendix-a-implementation-tickets)
+13. [Appendix B: XML Schema Definition](#appendix-b-xml-schema-definition)
+
+## Problem Definition
+
+### Background
+
+The Ed-Fi API Profiles feature is a critical capability that enables educational organizations to:
+
+- **Enforce data policies** on API resources based on operational or regulatory requirements
+- **Control visibility** of sensitive data elements to different API consumers
+- **Restrict update paths** to prevent unauthorized modifications
+- **Support multi-tenant scenarios** where different clients have different data access rights
+
+### Current State
+
+The Profiles feature currently exists in the AdminAPI-2.x codebase where:
+
+- Profiles are defined in XML format
+- Rules are evaluated at the API layer during request processing
+- Profile enforcement is tightly coupled with the API implementation
+- Profile management is manual and file-based
+
+### Target State
+
+Moving this capability into DMS will provide:
+
+- **Centralized policy enforcement** - consistent rules across all API operations
+- **Dynamic profile management** - database-backed profiles with runtime updates
+- **Scalability** - better performance with cached profile rules
+- **Maintainability** - separation of concerns between policy and business logic
+- **Consistency** - unified approach across the Ed-Fi ecosystem
+
+## Conceptual Design
+
+### Main Principles
+
+1. **Profile-Based Access Control**
+ - Profiles act as named policy documents that define resource access patterns
+ - Each profile specifies inclusion/exclusion rules for properties, references, and collections
+ - Multiple profiles can exist, supporting different consumer roles or scenarios
+
+2. **Database-Backed Storage**
+ - Profiles are imported from XML and stored in database tables
+
+3. **Runtime Enforcement**
+ - Profile resolution happens early in the request pipeline
+ - Rules are applied dynamically to filter request/response data
+ - Both read and write operations are subject to profile enforcement
+
+4. **Header-Based Selection**
+ - Clients specify profiles via Ed-Fi vendor-specific media types in HTTP headers
+ - `Accept` header for GET operations: `application/vnd.ed-fi.{resource}.{profile}.readable+json`
+ - `Content-Type` header for POST/PUT operations: `application/vnd.ed-fi.{resource}.{profile}.writable+json`
+ - Profile name and resource must be embedded in the media type (not as parameters)
+
+5. **Legacy Compatibility**
+ - XML remains the canonical format for profile definition
+ - Import/export maintains format compatibility with AdminAPI-2.x
+ - Existing profile documents can be migrated without modification
+
+### Key Components
+
+```mermaid
+graph TB
+ subgraph "API Client"
+ A[Data API Request]
+ B[HTTP Headers
Accept/Content-Type]
+ AUTH[Auth Token
Application ID]
+ end
+
+ subgraph "Config Service"
+ M[Config API
/v2/profiles/*]
+ APPS[Config API
/v2/applications/*]
+ N[Profile Repository]
+ APPREPO[ApplicationProfile
Repository]
+ O[Profile Storage
dmscs.Profile JSONB]
+ APPTBL[ApplicationProfile
Junction Table]
+ P[Profile Import/Export]
+ end
+
+ subgraph "DMS Pipeline"
+ C[Profile Resolver]
+ D[Config API Client]
+ E[Rules Engine]
+ F[Request Validator]
+ G[Response Filter]
+ H[Document Store]
+ end
+
+ A -->|1. Data Request| B
+ A -->|1a. With Token| AUTH
+ B -->|2a. Extract Profile Name| C
+ AUTH -->|2b. Extract App ID| C
+ C -->|3a. Fetch by Name| D
+ C -->|3b. Fetch by App ID| D
+ D -->|4a. GET profiles by name| M
+ D -->|4b. GET applications by id| APPS
+ M -->|5a. Query Profile| N
+ APPS -->|5b. Query App Profiles| APPREPO
+ N -->|6a. Load JSONB| O
+ APPREPO -->|6b. Join| APPTBL
+ APPREPO -->|6c. Load Profile| O
+ M -->|7a. Return Profile JSON| D
+ APPS -->|7b. Return Profiles Array| D
+ D -->|8. Profile Rules| E
+ E -->|9a. Validate Write| F
+ E -->|9b. Filter Read| G
+ F -->|10. Store| H
+ G -->|10. Retrieve| H
+
+ P -.->|Manage| M
+
+ style M fill:#e1f5ff
+ style APPS fill:#e1f5ff
+ style O fill:#e1f5ff
+ style APPTBL fill:#e1f5ff
+ style C fill:#fff4e1
+ style E fill:#fff4e1
+
+ Note1[Config Service owns
profile storage & API]
+ Note2[DMS fetches profiles via:
1. Header name or
2. Application assignment]
+```
+
+## Integration Strategy
+
+### Architecture Overview
+
+The Profiles feature is split across **Config Service** and **DMS**:
+
+**Config Service Responsibilities:**
+
+1. **Storage Layer** - PostgreSQL/MSSQL tables in `dmscs` schema
+2. **Repository Layer** - Data access abstractions for profile CRUD
+3. **API Layer** - Config API endpoints for profile management (`/v2/profiles/*`)
+
+**DMS Responsibilities:**
+
+1. **Client Layer** - HTTP client to fetch profiles from Config API
+2. **Service Layer** - Profile resolution and enforcement logic
+3. **Middleware Layer** - Pipeline integration for request/response processing
+
+**Key Architectural Points:**
+
+- **Config Service owns profiles** - All profile storage and management happens in Config Service
+- **DMS consumes via API** - DMS has no direct database access; fetches profiles via Config API
+- **Separation of concerns** - Config manages metadata, DMS enforces policies
+
+### Request Processing Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant DMS as DMS API
+ participant Auth as Auth Middleware
+ participant Resolver as Profile Resolver
+ participant ConfigAPI as Config API
+ participant ConfigDB as Config DB
+ participant Pipeline as DMS Pipeline
+ participant Backend as Document Store
+
+ Client->>DMS: HTTP Request (+ optional Profile Header)
+ DMS->>Auth: Authenticate
+ Auth-->>DMS: Token Claims (ApplicationId)
+
+ alt Profile from Header
+ DMS->>Resolver: Extract Profile Name from Header
+ Resolver->>ConfigAPI: GET /v2/profiles?name={name}
+ ConfigAPI->>ConfigDB: Query Profile by Name
+ else Profile from Application
+ DMS->>Resolver: Extract ApplicationId from Token
+ Resolver->>ConfigAPI: GET /v2/applications/{applicationId}
+ ConfigAPI->>ConfigDB: Query Application + Join ApplicationProfile
+ ConfigDB-->>ConfigAPI: Application with ProfileIds
+ ConfigAPI->>ConfigDB: Query Profiles by ProfileIds
+ end
+
+ ConfigDB-->>ConfigAPI: Profile Data (JSONB)
+ ConfigAPI-->>Resolver: Profile JSON
+ Resolver->>Pipeline: Attach Profile Context
+
+ alt Write Operation
+ Pipeline->>Pipeline: Validate Against Profile
+ Pipeline->>Backend: Store if Valid
+ else Read Operation
+ Pipeline->>Backend: Retrieve Data
+ Pipeline->>Pipeline: Filter Against Profile
+ Pipeline-->>Client: Filtered Response
+ end
+
+ Note over Resolver,ConfigAPI: Priority: Header > Application > None
Future: Add local cache in DMS to reduce Config API calls
+```
+
+### Pipeline Integration Points
+
+The profile enforcement integrates into the existing DMS pipeline at specific stages:
+
+1. **Early Pipeline** - Profile resolution (after authentication)
+2. **Validation Stage** - Write operation validation (before schema validation)
+3. **Processing Stage** - Data filtering (before/after backend operations)
+4. **Response Stage** - Response filtering (before serialization)
+
+## Database Schema Design
+
+### Design Philosophy
+
+The profile storage design prioritizes **simplicity and performance** for the primary use case: loading entire profiles for enforcement. Profiles are stored as JSONB documents in PostgreSQL, combining:
+
+- **Simple schema** - Single table (like AdminAPI-2.x)
+- **Fast retrieval** - One query loads complete profile
+- **Native JSON support** - PostgreSQL JSONB for efficient storage and parsing
+- **Easy caching** - Atomic profile documents
+
+### Profile Table
+
+**Location**: Config Service database (`dmscs` schema)
+
+```sql
+CREATE TABLE IF NOT EXISTS dmscs.Profile (
+ ProfileId BIGINT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY,
+ ProfileName VARCHAR(255) NOT NULL,
+ ProfileDefinition JSONB NOT NULL,
+ CreatedAt TIMESTAMP NOT NULL DEFAULT NOW(),
+ CreatedBy VARCHAR(256),
+ LastModifiedAt TIMESTAMP,
+ ModifiedBy VARCHAR(256),
+
+ -- Ensure ProfileDefinition is a valid JSON object
+ CONSTRAINT chk_profile_definition_valid
+ CHECK (jsonb_typeof(ProfileDefinition) = 'object')
+);
+
+-- Index for profile name lookups (primary use case)
+CREATE UNIQUE INDEX IF NOT EXISTS idx_profile_name ON dmscs.Profile (ProfileName);
+
+COMMENT ON COLUMN dmscs.Profile.ProfileId IS 'Profile unique identifier';
+COMMENT ON COLUMN dmscs.Profile.ProfileName IS 'Profile name';
+COMMENT ON COLUMN dmscs.Profile.ProfileDefinition IS 'Profile definition stored as JSONB';
+COMMENT ON COLUMN dmscs.Profile.CreatedAt IS 'Timestamp when the record was created (UTC)';
+COMMENT ON COLUMN dmscs.Profile.CreatedBy IS 'User or client ID who created the record';
+COMMENT ON COLUMN dmscs.Profile.LastModifiedAt IS 'Timestamp when the record was last modified (UTC)';
+COMMENT ON COLUMN dmscs.Profile.ModifiedBy IS 'User or client ID who last modified the record';
+```
+
+### Application-Profile Assignment Table
+
+**Purpose**: Links API clients/applications to profiles, enabling automatic profile assignment without explicit header specification.
+
+**Location**: Config Service database (`dmscs` schema)
+
+```sql
+CREATE TABLE IF NOT EXISTS dmscs.ApplicationProfile (
+ ApplicationId BIGINT NOT NULL,
+ ProfileId BIGINT NOT NULL,
+ CreatedAt TIMESTAMP NOT NULL DEFAULT NOW(),
+ CreatedBy VARCHAR(255),
+ LastModifiedAt TIMESTAMP,
+ ModifiedBy VARCHAR(255),
+
+ -- Composite primary key ensures unique application-profile relationships
+ PRIMARY KEY (ApplicationId, ProfileId),
+
+ -- Foreign key to Profile table
+ CONSTRAINT fk_applicationprofile_profile
+ FOREIGN KEY (ProfileId)
+ REFERENCES dmscs.Profile(ProfileId)
+ ON DELETE CASCADE
+
+ -- Foreign key to Application table
+ -- Note: ApplicationId references the application table
+ CONSTRAINT fk_applicationprofile_application
+ FOREIGN KEY (ApplicationId)
+ REFERENCES dmscs.Application(ApplicationId)
+ ON DELETE CASCADE
+);
+
+-- Index for profile lookups (reverse lookup: find apps using a profile)
+-- Note: ApplicationId is already indexed as part of the primary key
+CREATE INDEX IF NOT EXISTS idx_applicationprofile_profileid
+ ON dmscs.ApplicationProfile (ProfileId);
+
+COMMENT ON TABLE dmscs.ApplicationProfile IS 'Junction table linking applications to profiles for automatic profile assignment';
+COMMENT ON COLUMN dmscs.ApplicationProfile.ApplicationId IS 'Reference to the API client/application in the auth system';
+COMMENT ON COLUMN dmscs.ApplicationProfile.ProfileId IS 'Reference to the profile being assigned';
+COMMENT ON COLUMN dmscs.ApplicationProfile.CreatedAt IS 'Timestamp when the assignment was created (UTC)';
+COMMENT ON COLUMN dmscs.ApplicationProfile.CreatedBy IS 'User or client ID who created the assignment';
+COMMENT ON COLUMN dmscs.ApplicationProfile.LastModifiedAt IS 'Timestamp when the assignment was last modified (UTC)';
+COMMENT ON COLUMN dmscs.ApplicationProfile.ModifiedBy IS 'User or client ID who last modified the assignment';
+```
+
+### JSONB Structure
+
+Profile rules are stored in JSONB format, converted from the canonical XML format:
+
+```json
+{
+ "profileName": "Student-Read-Only",
+ "description": "Read-only access to basic student information",
+ "resources": [
+ {
+ "resourceName": "Student",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ {"name": "studentUniqueId"},
+ {"name": "firstName"},
+ {"name": "lastSurname"},
+ {"name": "birthDate"}
+ ],
+ "collections": [
+ {
+ "name": "addresses",
+ "memberSelection": "Exclude"
+ }
+ ],
+ "references": [
+ {
+ "name": "schoolReference",
+ "properties": [
+ {"name": "schoolId"}
+ ]
+ }
+ ]
+ },
+ "writeContentType": null
+ }
+ ]
+}
+```
+
+**Example with Collection Filters:**
+
+```json
+{
+ "profileName": "School-Filtered-Addresses",
+ "resources": [
+ {
+ "resourceName": "School",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ {"name": "schoolId"},
+ {"name": "nameOfInstitution"}
+ ],
+ "collections": [
+ {
+ "name": "educationOrganizationAddresses",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ {"name": "addressTypeDescriptor"},
+ {"name": "streetNumberName"},
+ {"name": "city"},
+ {"name": "stateAbbreviationDescriptor"},
+ {"name": "postalCode"}
+ ],
+ "filters": [
+ {
+ "propertyName": "addressTypeDescriptor",
+ "filterMode": "IncludeOnly",
+ "values": ["Physical", "Mailing"]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+### Schema Rationale
+
+**Why JSONB over Normalized Tables:**
+
+- **Primary Access Pattern**: Profiles are always loaded in full for enforcement
+- **No Complex Queries**: Don't need to query "which profiles affect property X"
+- **Atomic Operations**: Single-row updates prevent partial corruption
+- **Caching**: Entire profile fits in one cache entry
+- **Simplicity**: One table vs. 4+ normalized tables
+- **Performance**: Single query vs. multiple joins
+- **Compatibility**: Matches AdminAPI-2.x simplicity while improving storage
+
+**Why JSONB over XML Text:**
+
+- **Faster Parsing**: Native JSON support in C# (System.Text.Json)
+- **More Compact**: JSONB stores binary format (30-50% smaller than XML)
+- **Validation**: Built-in JSON constraint checking
+- **PostgreSQL Native**: Efficient storage and retrieval
+- **Modern Standard**: JSON is standard for REST APIs
+
+**Why Not GIN Indexing:**
+
+- Profiles are retrieved by name/ID, not by internal content
+- Full profile is always needed for enforcement
+- Index overhead not justified for this use case
+- Simpler = faster for the common case
+
+### SQL Server Compatibility
+
+For SQL Server deployments, the schema uses `NVARCHAR(MAX)` with JSON validation:
+
+```sql
+-- Profile table
+CREATE TABLE dmscs.Profile (
+ ProfileId BIGINT IDENTITY(1,1) PRIMARY KEY,
+ ProfileName NVARCHAR(255) NOT NULL,
+ ProfileDefinition NVARCHAR(MAX) NOT NULL,
+ CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ CreatedBy NVARCHAR(256),
+ LastModifiedAt DATETIME2,
+ ModifiedBy NVARCHAR(256),
+
+ -- Ensure ProfileDefinition is valid JSON
+ CONSTRAINT chk_profile_definition_valid
+ CHECK (ISJSON(ProfileDefinition) = 1)
+);
+
+CREATE UNIQUE INDEX idx_profile_name
+ ON dmscs.Profile (ProfileName);
+
+-- ApplicationProfile junction table
+CREATE TABLE dmscs.ApplicationProfile (
+ ApplicationId BIGINT NOT NULL,
+ ProfileId BIGINT NOT NULL,
+ CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ CreatedBy NVARCHAR(256),
+ LastModifiedAt DATETIME2,
+ ModifiedBy NVARCHAR(256),
+
+ -- Composite primary key
+ PRIMARY KEY (ApplicationId, ProfileId),
+
+ -- Foreign key to Profile table
+ CONSTRAINT fk_applicationprofile_profile
+ FOREIGN KEY (ProfileId)
+ REFERENCES dmscs.Profile(ProfileId)
+ ON DELETE CASCADE
+);
+
+-- Index for reverse lookup (find apps using a profile)
+CREATE INDEX idx_applicationprofile_profileid
+ ON dmscs.ApplicationProfile (ProfileId);
+```
+
+## Profile Resolution Flow
+
+### Resolution Algorithm
+
+**Phase 1 (Current Implementation - No Cache):**
+
+```mermaid
+flowchart TD
+ Start([Incoming Request]) --> CheckHeader{Profile Header
Present?}
+ CheckHeader -->|Yes| ExtractName[Extract Profile Name]
+ CheckHeader -->|No| CheckAssignment{Application Has
Assigned Profiles?}
+
+ CheckAssignment -->|Yes| LoadAssigned[Load Assigned Profiles
from ApplicationProfile]
+ CheckAssignment -->|No| NoProfile[No Profile Applied]
+
+ LoadAssigned --> SingleProfile{Single Profile
Assigned?}
+ SingleProfile -->|Yes| ExtractName
+ SingleProfile -->|Multiple| RequireHeader[Require Explicit Header]
+
+ RequireHeader --> Error400[400 Multiple Profiles,
Header Required]
+
+ ExtractName --> CallConfigAPI[HTTP GET to Config API
/v2/profiles?name=...]
+
+ CallConfigAPI --> ProfileExists{Profile Exists?}
+ ProfileExists -->|Yes| LoadProfile[Parse Profile JSON & Rules]
+ ProfileExists -->|No| CheckMethod{Request
Method?}
+
+ CheckMethod -->|GET| Error406[406 Not Acceptable
Profile Not Found]
+ CheckMethod -->|POST/PUT| Error415[415 Unsupported Media Type
Profile Not Found]
+
+ LoadProfile --> BuildContext[Build Profile Context]
+
+ BuildContext --> AttachToPipeline[Attach to Request Context]
+ NoProfile --> Continue[Continue Without Profile]
+ AttachToPipeline --> Continue
+ Continue --> End([Pipeline Processing])
+
+ Error400 --> End
+ Error406 --> End
+ Error415 --> End
+
+ style CallConfigAPI fill:#bbdefb
+ style BuildContext fill:#fff9c4
+ style LoadAssigned fill:#c8e6c9
+```
+
+**Phase 2 (Future with Cache):**
+
+```mermaid
+flowchart TD
+ Start([Incoming Request]) --> CheckHeader{Profile Header
Present?}
+ CheckHeader -->|Yes| ExtractName[Extract Profile Name]
+ CheckHeader -->|No| CheckApp{Application Has
Assigned Profiles?}
+
+ CheckApp -->|Yes| LoadApp[Load Application Profiles]
+ CheckApp -->|No| NoProfile[No Profile Applied]
+
+ LoadApp --> SingleProfile{Single Profile?}
+ SingleProfile -->|Yes| ExtractName
+ SingleProfile -->|No| NoProfile
+
+ ExtractName --> LookupCache{Profile in Cache?}
+
+ LookupCache -->|Yes| ValidateCache{Cache Valid?}
+ LookupCache -->|No| CallConfigAPI[HTTP GET to Config API]
+
+ ValidateCache -->|Yes| LoadFromCache[Load from Cache]
+ ValidateCache -->|No| CallConfigAPI
+
+ CallConfigAPI --> ProfileExists{Profile Exists?}
+ ProfileExists -->|Yes| LoadProfile[Parse Profile JSON & Rules]
+ ProfileExists -->|No| CheckMethod{Request
Method?}
+
+ CheckMethod -->|GET| Error406[406 Not Acceptable
Profile Not Found]
+ CheckMethod -->|POST/PUT| Error415[415 Unsupported Media Type
Profile Not Found]
+
+ LoadProfile --> UpdateCache[Update Cache]
+ UpdateCache --> BuildContext[Build Profile Context]
+ LoadFromCache --> BuildContext
+
+ BuildContext --> AttachToPipeline[Attach to Request Context]
+ NoProfile --> Continue[Continue Without Profile]
+ AttachToPipeline --> Continue
+ Continue --> End([Pipeline Processing])
+
+ Error406 --> End
+ Error415 --> End
+
+ style LookupCache fill:#e1bee7
+ style UpdateCache fill:#e1bee7
+ style LoadFromCache fill:#e1bee7
+```
+
+### DMS Profile Resolution Strategy
+
+**Profile Selection Priority** (evaluated in order):
+
+1. **Explicit Header** - Profile specified in HTTP header (Accept/Content-Type)
+2. **Application Assignment** - Profile(s) assigned to the authenticated client/application
+3. **No Profile** - Request proceeds without profile enforcement
+
+**Application-Based Profile Assignment:**
+
+When no explicit profile header is provided, DMS:
+
+1. Identifies the authenticated application/client from the auth token
+2. Queries assigned profiles from `ApplicationProfile` table via Config API
+3. If single profile assigned: automatically applies that profile
+4. If multiple profiles assigned and you send a request without a specific profile header: Status Code: 403 Forbidden (The API views this as a Security Policy Failure, not a "Bad Request" (syntax error). Because it cannot determine which security restrictions to apply)
+5. If no profiles assigned: request proceeds without profile enforcement
+6. If profile name is a typo / doesn't exist in the system: 415 Unsupported Media Type (POST/PUT) or 406 Not Acceptable (GET).
+
+This matches Ed-Fi ODS API behavior where profiles can be pre-assigned to API consumers.
+
+**Current Approach (Phase 1):**
+
+- DMS fetches profiles from Config API on every request
+- Profile name extracted from HTTP headers OR application assignments
+- Direct HTTP call to Config API: `GET /v2/profiles?name={profileName}` or `GET /v2/applications/{id}` (with profile data)
+- Config API returns profile JSON from `dmscs.Profile` table
+- DMS parses and applies rules immediately
+
+**Future Enhancement (Phase 2 - Recommended):**
+
+- Add local caching in DMS to reduce Config API calls
+- **Cache Key**: `profile:{profileName}` or `app:{applicationId}:profiles`
+- **Cache Value**: Complete parsed profile object(s)
+- **Cache Duration**: Configurable (default: 5-15 minutes)
+- **Invalidation**: Time-based expiration (eventual consistency acceptable)
+- **Benefits**: Reduce latency, decrease load on Config API, improve throughput
+- **Consideration**: Profile updates will have propagation delay to DMS instances
+
+## Enforcement Pipeline Architecture
+
+### Middleware Stack
+
+```mermaid
+graph LR
+ subgraph "Request Flow"
+ A[Incoming Request] --> B[Authentication]
+ B --> C[Profile Resolution]
+ C --> D[Profile Validation]
+ D --> E[Schema Validation]
+ E --> F[Business Logic]
+ F --> G[Data Access]
+ end
+
+ subgraph "Response Flow"
+ G --> H[Profile Filtering]
+ H --> I[Response Formation]
+ I --> J[Outgoing Response]
+ end
+
+ style C fill:#ffeb3b
+ style D fill:#ffeb3b
+ style H fill:#ffeb3b
+```
+
+### Write Operation Enforcement
+
+For POST/PUT operations:
+
+1. **Profile Resolution**: Extract profile from `Content-Type` header (Ed-Fi vendor media type format: `application/vnd.ed-fi.{resource}.{profile}.writable+json`)
+2. **Rule Evaluation**: Load property/collection/reference/filter rules
+3. **Input Validation**:
+ - Check for excluded properties in request body
+ - Validate included properties are not missing (if required)
+ - Verify collection items conform to profile rules
+ - Apply collection item filters (validate descriptor/type values)
+ - Ensure references are allowed
+4. **Rejection**: Return 400 Bad Request if validation fails
+5. **Processing**: Continue to backend if validation passes
+
+**Collection Filter Validation Example:**
+
+If a profile specifies a filter on `AddressTypeDescriptor` with `filterMode="IncludeOnly"` and values `["Physical", "Mailing"]`, then a POST request containing an address with `AddressTypeDescriptor="Temporary"` will be rejected with a 400 Bad Request error.
+
+### Read Operation Enforcement
+
+For GET operations:
+
+1. **Profile Resolution**: Extract profile from `Accept` header (Ed-Fi vendor media type format: `application/vnd.ed-fi.{resource}.{profile}.readable+json`)
+2. **Rule Loading**: Retrieve filtering rules for resource
+3. **Data Retrieval**: Fetch data from backend (unfiltered)
+4. **Response Filtering**:
+ - Remove excluded properties from response
+ - Filter collections based on collection rules
+ - Apply collection item filters (remove items not matching filter criteria)
+ - Apply reference filtering
+ - Maintain data structure integrity
+5. **Serialization**: Return filtered response
+
+**Collection Filter Processing Example:**
+
+If a profile specifies a filter on `AddressTypeDescriptor` with `filterMode="IncludeOnly"` and values `["Physical", "Mailing"]`, then a GET response will only include addresses where `AddressTypeDescriptor` is either "Physical" or "Mailing". All other address items are removed from the response.
+
+### Error Handling
+
+- **Profile Not Found (GET)**: HTTP 406 Not Acceptable - when profile specified in Accept header does not exist
+- **Profile Not Found (POST/PUT)**: HTTP 415 Unsupported Media Type - when profile specified in Content-Type header does not exist
+- **Invalid Profile Rules**: HTTP 500 with error details logged
+- **Validation Failures**: HTTP 400 with specific rule violations
+- **Ambiguous Profile**: HTTP 400 when multiple profiles match (application has multiple profiles assigned and no explicit header provided)
+
+## XML to JSON Conversion Strategy
+
+### Conversion Philosophy
+
+The profile storage uses **JSON as the internal format** while maintaining **XML as the external format** for compatibility with AdminAPI-2.x. This provides the best of both worlds:
+
+- **XML Compatibility**: Import/export maintains AdminAPI-2.x format
+- **JSON Efficiency**: Internal storage and processing uses modern JSON
+- **Transparent**: Users work with XML, system works with JSON
+
+### Conversion Process
+
+```
+Import Flow:
+XML File → XML Parser → XML Validator → JSON Converter → JSONB Storage
+
+Export Flow:
+JSONB Storage → JSON Loader → XML Converter → XML Validator → XML File
+```
+
+### Mapping Example
+
+**XML Input:**
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+**JSONB Storage:**
+
+```json
+{
+ "profileName": "Student-Read-Only",
+ "resources": [
+ {
+ "resourceName": "Student",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "studentUniqueId" },
+ { "name": "firstName" }
+ ],
+ "collections": [
+ { "name": "addresses", "memberSelection": "ExcludeAll" }
+ ]
+ }
+ }
+ ]
+}
+```
+
+**XML Input with Filters:**
+
+```xml
+
+
+
+
+
+
+
+
+ Physical
+ Mailing
+
+
+
+
+
+```
+
+**JSONB Storage with Filters:**
+
+```json
+{
+ "profileName": "School-Filtered",
+ "resources": [
+ {
+ "resourceName": "School",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "schoolId" }
+ ],
+ "collections": [
+ {
+ "name": "educationOrganizationAddresses",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "streetNumberName" },
+ { "name": "city" }
+ ],
+ "filters": [
+ {
+ "propertyName": "addressTypeDescriptor",
+ "filterMode": "IncludeOnly",
+ "values": ["Physical", "Mailing"]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+**XML Input with Object (Embedded Reference):**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+**JSONB Storage with Object:**
+
+```json
+{
+ "profileName": "Student-With-School",
+ "resources": [
+ {
+ "resourceName": "Student",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "studentUniqueId" },
+ { "name": "firstName" },
+ { "name": "lastName" }
+ ],
+ "objects": [
+ {
+ "name": "schoolYearTypeReference",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "schoolYear" }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+**XML Input with Extension:**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**JSONB Storage with Extension:**
+
+```json
+{
+ "profileName": "Student-With-Extension",
+ "resources": [
+ {
+ "resourceName": "Student",
+ "logicalSchema": "edfi",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "studentUniqueId" },
+ { "name": "firstName" }
+ ],
+ "extensions": [
+ {
+ "name": "Sample",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "graduationYear" }
+ ],
+ "objects": [
+ {
+ "name": "petPreference",
+ "memberSelection": "IncludeOnly",
+ "logicalSchema": "sample",
+ "properties": [
+ { "name": "petType" },
+ { "name": "petName" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+**XML Input with Complex Nested Structure:**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Physical
+ Mailing
+
+
+
+
+
+
+ Archived
+
+
+
+
+
+
+```
+
+**JSONB Storage with Complex Nested Structure:**
+
+```json
+{
+ "profileName": "School-Complex",
+ "resources": [
+ {
+ "resourceName": "School",
+ "logicalSchema": "edfi",
+ "readContentType": {
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "schoolId" },
+ { "name": "nameOfInstitution" }
+ ],
+ "collections": [
+ {
+ "name": "schoolCategories",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "schoolCategoryDescriptor" }
+ ]
+ },
+ {
+ "name": "educationOrganizationAddresses",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "streetNumberName" },
+ { "name": "city" },
+ { "name": "stateAbbreviationDescriptor" }
+ ],
+ "objects": [
+ {
+ "name": "periods",
+ "memberSelection": "ExcludeOnly",
+ "properties": [
+ { "name": "beginDate" },
+ { "name": "endDate" }
+ ]
+ }
+ ],
+ "filters": [
+ {
+ "propertyName": "addressTypeDescriptor",
+ "filterMode": "IncludeOnly",
+ "values": ["Physical", "Mailing"]
+ }
+ ]
+ }
+ ],
+ "extensions": [
+ {
+ "name": "Sample",
+ "memberSelection": "IncludeOnly",
+ "properties": [
+ { "name": "accreditationStatus" }
+ ],
+ "collections": [
+ {
+ "name": "programs",
+ "memberSelection": "IncludeAll",
+ "logicalSchema": "sample",
+ "filters": [
+ {
+ "propertyName": "programType",
+ "filterMode": "ExcludeOnly",
+ "values": ["Archived"]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+### Conversion Rules
+
+1. **Element to Object**: XML elements become JSON objects
+2. **Attributes to Properties**: XML attributes become JSON properties (e.g., `name`, `memberSelection`, `logicalSchema`, `propertyName`, `filterMode`)
+3. **Repeated Elements to Arrays**: Multiple same-named elements become arrays (e.g., `Property` → `properties[]`, `Object` → `objects[]`, `Collection` → `collections[]`, `Extension` → `extensions[]`)
+4. **Case Normalization**: XML PascalCase → JSON camelCase (internally)
+5. **Null Handling**: Empty XML elements → null in JSON (or omitted)
+6. **Filter Elements**: `` elements with `` children convert to filter objects with `propertyName`, `filterMode`, and `values` array
+7. **Object Elements**: `