Status: User-facing reference - Last verified: 2026-10-21 - Product version:
0.55.67
Production-ready, multi-tenant SCIM 2.0 server for Microsoft Entra ID provisioning and any RFC 7643/7644-compliant identity client.
What's new and how to upgrade: User-facing release notes cover the proposed 0.55.67 tenant 10 dev-and-canary migration, earlier correction history, cumulative changes since 0.55.20, migration/rollback steps and known limits. See CHANGELOG.md for detailed engineering history.
0.55.67 migration release preparation: the operator authorized a performance-admission exception for this dev-and-canary migration only. Performance remains unqualified; every other gate remains mandatory. Customer production must not change. The migration execution record records the scope and pending final artifact, target acceptance and cutover. No deployed release is claimed.
Current Hono source integration: Actual Prisma parent qualification records proposed 0.55.66 integrating reviewed master after #259, #260 and #261 merged. It preserves the bounded adapter compatibility proof and all dependency fixes. New-head validation and review remain required; no deployed release or full advisory/performance admission is claimed.
Merged API advisory source: Consumer compatibility and historical Hono boundary
records the 0.55.64 qs and fast-uri correction merged at cffc64c8.
Merged source-only inventory correction: API override-context validity describes merged 0.55.63 source and the complete-tree npm regression. It changes no application behavior and does not qualify a deployment or clear the separately tracked API security advisories.
For actual supported shapes and integration limits, see SCIM/Entra compatibility and the 39-setting behavior/evidence matrix. Keep strict validation enabled for Entra; diagnose the request and effective schema before applying a targeted compatibility setting.
- Overview
- Key Features
- Architecture
- Quick Start
- Deployment Options
- SCIM 2.0 Compliance
- Authentication
- Multi-Tenant Endpoints
- Endpoint Profiles & Presets
- Configuration Flags
- API Reference (Summary)
- API Examples
- SCIM Filter Support
- Attribute Projection
- Bulk Operations
- Observability & Logging
- Web Admin UI
- Environment Variables
- Testing
- Technology Stack
- Project Structure
- Documentation Index
- Contributing
SCIMServer is a fully RFC-compliant SCIM 2.0 server built with NestJS and PostgreSQL. It provides isolated, configurable multi-tenant endpoints - each with its own schema, resource types, authentication, and behavioral settings. Designed for Microsoft Entra ID provisioning but compatible with any SCIM client.
Use cases:
- Test and validate Entra ID SCIM provisioning configurations
- Develop and debug custom SCIM connectors
- Run compliance tests against a known-good SCIM implementation
- Provision users/groups from any SCIM-compliant identity provider
| Category | Capabilities |
|---|---|
| SCIM Protocol | Full RFC 7643/7644 - Users, Groups, custom resource types, PATCH (add/replace/remove), PUT, Bulk, /Me, .search, filtering, sorting, pagination, ETag, attribute projection |
| Multi-Tenant | Isolated endpoints with independent schemas, resource types, config flags, credentials, and log streams |
| Authentication | 3-tier chain: per-endpoint bcrypt tokens - OAuth 2.0 JWT - global bearer secret |
| Schema Engine | 6 built-in presets, tighten-only customization, auto-expand from RFC baselines, strict validation |
| Observability | Structured JSON logging, SSE live stream, ring buffer, per-endpoint log isolation, file rotation, auto-prune |
| Deployment | Docker Compose (1 command), Azure Container Apps (1 script), local dev, pre-built GHCR image |
| Web UI | React + Vite admin console - dashboard, endpoint CRUD, Workbench, Bulk UI, custom resource types, discovery diff, operations, logs, settings |
| Testing | 5,192 API unit + 1,533 API E2E + 1,567 web Vitest + 1,532 live SCIM + 6 focused Playwright = 9,830 checks, measured on 2026-09-24 against v0.55.34 code and the v0.55.33 dev API |
+-------------------------------------------------------------------------+
| SCIMServer (NestJS) |
| +-------+ +------+ +---------+ +--------+ +-------+ +----------+ |
| | Auth | | SCIM | | Admin | | Logging| | OAuth | | Web UI | |
| | Guard | | Core | | Console | | Engine | | Token | | (React) | |
| +---+---+ +--+---+ +----+----+ +---+----+ +---+---+ +----+-----+ |
| | | | | | | |
| +---+---------+-----------+------------+-----------+-----------+---+ |
| | Endpoint Service (Multi-Tenant Cache) | |
| +------------------------------------------------------------------+ |
| | | |
| +---+------+ +------+-------+ |
| | Prisma | | InMemory | |
| | (PG 17) | | (Dev/Test) | |
| +----------+ +--------------+ |
+-------------------------------------------------------------------------+
8 tables in PostgreSQL 17 (extensions citext, pgcrypto, pg_trgm). RequestLog
is the durable request/audit trail; it is deliberately not a foreign key to
Endpoint (its endpointId is a correlation column) so an append-only audit row
survives the deletion of the endpoint it describes.
erDiagram
Endpoint ||--o{ ScimResource : "owns (cascade)"
Endpoint ||--o{ EndpointCredential : "authenticates (cascade)"
ScimResource ||--o{ ResourceMember : "group has (cascade)"
ScimResource ||--o{ ResourceMember : "member of (set null)"
Endpoint }o..o{ RequestLog : "correlated by endpointId (NO FK)"
Endpoint {
uuid id PK
string name UK
string displayName
string description
jsonb profile "schemas/resourceTypes/spConfig/settings"
boolean active
datetime createdAt
datetime updatedAt
}
ScimResource {
uuid id PK
uuid endpointId FK
string resourceType "User/Group/custom"
uuid scimId
text externalId "caseExact"
citext userName
citext displayName
boolean active
jsonb payload "full SCIM resource"
int version "ETag"
string meta
datetime createdAt
datetime updatedAt
}
ResourceMember {
uuid id PK
uuid groupResourceId FK
uuid memberResourceId FK "nullable, set null"
string value
string type
string display
datetime createdAt
}
EndpointCredential {
uuid id PK
uuid endpointId FK
string credentialType "bearer/oauth_client/wif"
string credentialHash "bcrypt"
string label
jsonb metadata "clientId/scopes/WIF trust"
string secretEnvelope "DEK-encrypted, opt-in"
boolean active
datetime expiresAt
datetime createdAt
}
RequestLog {
uuid id PK
uuid endpointId "correlation, NOT a FK"
string endpointName "display-name snapshot"
string method
string url
int status
int durationMs
string requestHeaders
string requestBody
string responseHeaders
string responseBody
string errorMessage
string errorStack
string identifier "derived userName/displayName"
uuid requestId "X-Request-Id correlator"
string authOutcome "accept/reject"
string authMethod
string authReason
uuid authCredentialId
string authDecision "full redacted AuthDecisionTrace JSON"
datetime createdAt
}
JwksHostAllowlistEntry {
uuid id PK
string host UK
string label
datetime createdAt
}
CredentialDek {
uuid id PK
string wrappedDek "KEK-wrapped DEK"
string kekSalt
boolean active
datetime createdAt
}
ServerSetting {
string key PK
string value
datetime updatedAt
}
Log lifecycle.
RequestLogrows are written by a buffered batch writer and pruned by age (auto-prune, default 21-day retention viaLOG_RETENTION_DAYS), not by endpoint deletion. A row whose endpoint was deleted keeps itsendpointIdand stays queryable by it; the endpoint name simply no longer resolves in the UI. See docs/LOGGING_AND_OBSERVABILITY.md.
sequenceDiagram
participant C as SCIM Client
participant G as Auth Guard
participant M as Middleware
participant Ctrl as Controller
participant Svc as Service
participant V as Schema Validator
participant PE as Patch Engine
participant R as Repository
participant DB as PostgreSQL
C->>G: POST /scim/endpoints/{id}/Users<br>Authorization: Bearer {token}
G->>G: 1. Check Public decorator<br>2. Per-endpoint bcrypt<br>3. OAuth JWT<br>4. Shared secret
G-->>C: 401 if all fail
G->>M: Authenticated request
M->>M: X-Request-Id (UUID)<br>Content-Type check<br>Endpoint context (ALS)
M->>Ctrl: Validated request
Ctrl->>Svc: createUser(body, endpointId)
Svc->>Svc: Resolve endpoint profile<br>Check endpoint active
Svc->>V: Validate against endpoint schema<br>(required, type, mutability, uniqueness)
V-->>Svc: Validation result
Svc->>Svc: Strip readOnly attributes<br>Boolean string coercion<br>Primary enforcement
Svc->>R: create(endpointId, user)
R->>DB: INSERT INTO ScimResource
DB-->>R: Created row
R-->>Svc: User model
Svc->>Svc: Build response<br>(meta, location, schemas[], projection)
Svc-->>Ctrl: SCIM User resource
Ctrl-->>C: 201 Created<br>Content-Type: application/scim+json<br>Location: .../Users/{id}<br>ETag: W/"1"
git clone https://github.com/your-org/SCIMServer.git
cd SCIMServer
docker compose up --build -dThe server starts at http://localhost:8080 with PostgreSQL 17.
Those remain the default host bindings. When either port is already in use,
set API_HOST_PORT and POSTGRES_HOST_PORT before docker compose up.
Container names can likewise be isolated with API_CONTAINER_NAME and
POSTGRES_CONTAINER_NAME; BIND_HOST_IP optionally restricts both published
ports to one host interface.
# Verify health
curl http://localhost:8080/health
# Create an endpoint
curl -X POST http://localhost:8080/scim/admin/endpoints \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{"name": "my-tenant", "profilePreset": "entra-id"}'
# Create a user on that endpoint
curl -X POST http://localhost:8080/scim/endpoints/{endpointId}/Users \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "jane.doe@example.com",
"displayName": "Jane Doe",
"active": true
}'cd api
npm ci
npx prisma generate
npx prisma migrate deploy # requires PostgreSQL running on localhost:5432
npm run start:dev # http://localhost:3000irm https://raw.githubusercontent.com/your-org/SCIMServer/main/bootstrap.ps1 | iexOr manually:
.\deploy.ps1| Method | Command | URL | Database |
|---|---|---|---|
| Docker Compose | docker compose up --build -d |
http://localhost:8080 | PostgreSQL 17 (containerized) |
| Local Dev | cd api && npm run start:dev |
http://localhost:3000 | PostgreSQL on localhost:5432 |
| Azure Container Apps | .\deploy.ps1 or irm .../bootstrap.ps1 | iex |
https://{app}.azurecontainerapps.io | Azure Flexible Server |
| Pre-built Image | docker pull ghcr.io/your-org/scimserver:latest |
http://localhost:8080 | Bring your own PG |
# docker-compose.yml
services:
postgres:
image: postgres:17
environment:
POSTGRES_DB: scimdb
POSTGRES_USER: scim
POSTGRES_PASSWORD: scim
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U scim -d scimdb"]
api:
build: .
ports:
- "8080:8080"
environment:
PERSISTENCE_BACKEND: prisma
DATABASE_URL: postgresql://scim:scim@postgres:5432/scimdb
SCIM_SHARED_SECRET: changeme-scim
JWT_SECRET: changeme-jwt
OAUTH_CLIENT_SECRET: changeme-oauth
depends_on:
postgres:
condition: service_healthyFull compliance with RFC 7643 (Core Schema) and RFC 7644 (Protocol).
| RFC Feature | Section | Status | Notes |
|---|---|---|---|
| User CRUD | 7644 S3.1-3.6 | Full | POST/GET/PUT/PATCH/DELETE |
| Group CRUD | 7644 S3.1-3.6 | Full | Including member management |
| Custom Resource Types | 7643 S6 | Full | Register via endpoint profile |
| PATCH Operations | 7644 S3.5.2 | Full | add/replace/remove, multi-op, valuePath, dot-notation |
| Bulk Operations | 7644 S3.7 | Full | POST/PUT/PATCH/DELETE, bulkId cross-ref, failOnErrors |
| Filtering | 7644 S3.4.2.2 | Full | eq/ne/co/sw/ew/gt/ge/lt/le/pr, AND/OR/NOT, valuePath |
| Sorting | 7644 S3.4.2.3 | Full | sortBy/sortOrder on any attribute |
| Pagination | 7644 S3.4.2.4 | Full | startIndex (1-based), count, totalResults |
| Attribute Projection | 7644 S3.4.2.5 | Full | attributes, excludedAttributes query params |
| POST .search | 7644 S3.4.3 | Full | Server-side search via POST body |
| Discovery | 7644 S4 | Full | /Schemas, /ResourceTypes, /ServiceProviderConfig |
| ETag / Conditional | 7644 S3.14 | Full | If-Match, If-None-Match, W/ weak ETags |
| /Me Endpoint | 7644 S3.11 | Full | OAuth JWT sub claim resolution |
| Content-Type | 7644 S3.1 | Full | application/scim+json + application/json |
| Error Format | 7644 S3.12 | Full | SCIM error schema, scimType, detail, diagnostics |
| Schema Validation | 7643 S2 | Full | Required, type, mutability, uniqueness, returned, canonical |
| Case Insensitivity | 7643 S2.1 | Full | Attribute names, URNs, filter values (CITEXT) |
| Multi-Valued Attrs | 7643 S2.4 | Full | Primary enforcement (normalize/reject/passthrough) |
SCIMServer implements a 3-tier authentication chain. Each request is evaluated against all tiers in order until one succeeds.
flowchart TD
A["Incoming request"] --> B{"Public route?"}
B -->|Yes| Z[Allow]
B -->|No| C{"URL has /endpoints/uuid/?"}
C -->|Yes| D{"Bearer or OAuth method enabled?"}
D -->|Yes| E["Verify keyed token or legacy bcrypt hash"]
E -->|Match| Z
E -->|No match| F
D -->|No| F
C -->|No| F
F{"Token != shared secret?"} -->|Yes| G["Validate as OAuth JWT"]
G -->|Valid| Z
G -->|Invalid| H
F -->|No| H
H{"Token == SCIM_SHARED_SECRET?"} -->|Yes| Z
H -->|No| I["401 Unauthorized<br/>WWW-Authenticate: Bearer"]
# Create a credential for an endpoint
curl -X POST http://localhost:8080/scim/admin/endpoints/{id}/credentials \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{
"label": "entra-prod",
"credentialType": "bearer"
}'
# Response includes one-time plaintext token
# {"id":"...","token":"scim_ep_a1b2c3...","credentialType":"bearer",...}
# Use the scoped token for SCIM operations
curl http://localhost:8080/scim/endpoints/{id}/Users \
-H "Authorization: Bearer scim_ep_a1b2c3..."- Current tokens use keyed HMAC verification; pre-migration bcrypt tokens remain valid
- Requires
SecretTokenBearerAuthEnabled: truein endpoint settings - Supports optional
expiresAtfor time-limited tokens
# Get an access token
curl -X POST http://localhost:8080/scim/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "scimserver-client",
"client_secret": "changeme-oauth",
"scope": "scim.read scim.write"
}'
# Response
# {"access_token":"eyJhbGci...","token_type":"bearer","expires_in":3600,"scope":"scim.read scim.write"}
# Use the JWT
curl http://localhost:8080/scim/endpoints/{id}/Users \
-H "Authorization: Bearer eyJhbGci..."- JWT signed with
JWT_SECRET, 1-hour expiry - Scopes:
scim.read,scim.write,scim.manage - Required for
/Meendpoint (resolvessubclaim to userName)
curl http://localhost:8080/scim/endpoints/{id}/Users \
-H "Authorization: Bearer changeme-scim"- Set via
SCIM_SHARED_SECRETenv var - Required in production (
NODE_ENV=production), auto-generated in dev
These routes require no authentication per RFC 7644 S4:
| Route | Purpose |
|---|---|
GET /health |
Health check |
GET /scim/Schemas |
Global schema discovery |
GET /scim/ResourceTypes |
Global resource type discovery |
GET /scim/ServiceProviderConfig |
Service provider config |
GET /scim/endpoints/{id}/Schemas |
Endpoint-scoped schema discovery |
GET /scim/endpoints/{id}/ResourceTypes |
Endpoint-scoped resource types |
GET /scim/endpoints/{id}/ServiceProviderConfig |
Endpoint-scoped SPC |
POST /scim/oauth/token |
OAuth token exchange |
Every SCIM operation is scoped to an endpoint - an isolated tenant with its own users, groups, schema, config flags, credentials, and log stream.
flowchart LR
subgraph Endpoint A: entra-prod
UA[Users A]
GA[Groups A]
SA[Schema A]
CA[Credentials A]
end
subgraph Endpoint B: entra-staging
UB[Users B]
GB[Groups B]
SB[Schema B]
CB[Credentials B]
end
subgraph Endpoint C: okta-test
UC[Users C]
GC[Groups C]
SC[Schema C]
CC[Credentials C]
end
Client1[Entra ID Prod] -->|SCIM| UA
Client2[Entra ID Staging] -->|SCIM| UB
Client3[Okta] -->|SCIM| UC
# 1. Create endpoint with a preset
curl -X POST http://localhost:8080/scim/admin/endpoints \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{
"name": "my-tenant",
"displayName": "My Tenant",
"description": "Production Entra ID provisioning target",
"profilePreset": "entra-id"
}'
# Response (201 Created)
{
"id": "a1b2c3d4-...",
"name": "my-tenant",
"displayName": "My Tenant",
"description": "Production Entra ID provisioning target",
"active": true,
"scimBasePath": "/scim/endpoints/a1b2c3d4-.../",
"profile": { ... },
"createdAt": "2026-04-24T10:00:00.000Z",
"updatedAt": "2026-04-24T10:00:00.000Z"
}# 2. List endpoints
curl http://localhost:8080/scim/admin/endpoints?view=summary \
-H "Authorization: Bearer changeme-scim"
# 3. Update endpoint settings
curl -X PATCH http://localhost:8080/scim/admin/endpoints/{id} \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{"profile": {"settings": {"RequireIfMatch": true}}}'
# 4. Deactivate endpoint (blocks SCIM operations, returns 403)
curl -X PATCH http://localhost:8080/scim/admin/endpoints/{id} \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{"active": false}'
# 5. Get endpoint stats
curl http://localhost:8080/scim/admin/endpoints/{id}/stats \
-H "Authorization: Bearer changeme-scim"
# Response
# {"users": 150, "groups": 12, "groupMembers": 340, "requestLogs": 4200}
# 6. Delete endpoint (cascades all resources, logs, credentials)
curl -X DELETE http://localhost:8080/scim/admin/endpoints/{id} \
-H "Authorization: Bearer changeme-scim"Each endpoint has a profile defining its SCIM schema, resource types, service provider capabilities, and behavioral settings. Profiles can be created from built-in presets or defined inline with shorthand syntax.
| Preset | Default | Groups | Enterprise User | Extensions | Bulk | Sort | Description |
|---|---|---|---|---|---|---|---|
entra-id |
Yes | Yes | Yes | 4 Microsoft test extensions | No | No | Full Entra ID provisioning |
entra-id-minimal |
No | Yes | Yes | 4 Microsoft test extensions | No | No | Core identity fields only |
rfc-standard |
No | Yes | Yes | None | Yes (1000 ops) | Yes | Full RFC 7643/7644 compliance |
minimal |
No | Yes | No | None | No | No | Bare minimum for testing |
user-only |
No | No | Yes | None | No | Yes | User provisioning only |
user-only-with-custom-ext |
No | No | Yes | Custom extension (writeOnly attrs) | No | Yes | Custom extension demo |
# List all presets
curl http://localhost:8080/scim/admin/endpoints/presets \
-H "Authorization: Bearer changeme-scim"
# Get preset details
curl http://localhost:8080/scim/admin/endpoints/presets/entra-id \
-H "Authorization: Bearer changeme-scim"A profile has 4 sections:
{
"schemas": [
{
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
"name": "User",
"attributes": "all"
}
],
"resourceTypes": [
{
"id": "User",
"name": "User",
"endpoint": "/Users",
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
"schemaExtensions": [
{
"schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"required": false
}
]
}
],
"serviceProviderConfig": {
"patch": {
"supported": true
},
"bulk": {
"supported": false
},
"filter": {
"supported": true,
"maxResults": 200
},
"sort": {
"supported": false
},
"etag": {
"supported": true
},
"changePassword": {
"supported": false
}
},
"settings": {
"StrictSchemaValidation": true,
"AllowAndCoerceBooleanStrings": true,
"PrimaryEnforcement": "normalize"
}
}When "attributes": "all" is used, the engine expands it to the full RFC 7643 attribute list for that schema. For known RFC schemas, partial attribute definitions are merged with the RFC baseline - your overrides win, missing fields are filled from the standard.
Attribute characteristic overrides are validated to only move in the "tighter" direction versus RFC baselines:
| Characteristic | Allowed Direction |
|---|---|
required |
false - true only |
mutability |
readWrite - immutable - readOnly only |
uniqueness |
none - server - global only |
caseExact |
false - true only |
type |
Cannot be changed |
multiValued |
Cannot be changed |
Each endpoint has behavioral settings configured via profile.settings:
| Flag | Type | Default | Description |
|---|---|---|---|
StrictSchemaValidation |
boolean | true |
Require extension URNs in request schemas[] array |
AllowAndCoerceBooleanStrings |
boolean | true |
Coerce "True"/"False" strings to booleans (Entra ID compat) |
UserSoftDeleteEnabled |
boolean | true |
PATCH {active:false} deactivates instead of deleting |
UserHardDeleteEnabled |
boolean | true |
DELETE permanently removes user |
GroupHardDeleteEnabled |
boolean | true |
DELETE permanently removes group |
MultiMemberPatchOpForGroupEnabled |
boolean | true |
Multi-member add/remove in single PATCH op |
PatchOpAllowRemoveAllMembers |
boolean | false |
Allow remove with path=members (removes all) |
VerbosePatchSupported |
boolean | false |
Enable dot-notation PATCH paths (e.g., name.givenName) |
SchemaDiscoveryEnabled |
boolean | true |
Endpoint-scoped discovery endpoints respond (vs 404) |
RequireIfMatch |
boolean | false |
Mandate ETag If-Match on PUT/PATCH/DELETE |
SecretTokenBearerAuthEnabled |
boolean | false |
Enable per-endpoint bearer tokens |
OAuthClientCredentialsAuthEnabled |
boolean | false |
Enable per-endpoint OAuth clients |
WifCredentialsEnabled |
boolean | false |
Enable workload identity federation |
SharedSecretBearerAuthEnabled |
boolean | true |
Accept the server-global shared secret on this endpoint |
IncludeWarningAboutIgnoredReadOnlyAttribute |
boolean | false |
Emit warning when stripping readOnly attributes |
IgnoreReadOnlyAttributesInPatch |
boolean | false |
Strip readOnly PATCH ops silently instead of 400 |
PrimaryEnforcement |
string | passthrough |
Primary sub-attribute handling: normalize, reject, or passthrough |
logLevel |
string | (global) | Per-endpoint log level override: TRACE/DEBUG/INFO/WARN/ERROR/FATAL |
logFileEnabled |
boolean | true |
Per-endpoint log file under logs/endpoints/ |
curl -X PATCH http://localhost:8080/scim/admin/endpoints/{id} \
-H "Authorization: Bearer changeme-scim" \
-H "Content-Type: application/json" \
-d '{
"profile": {
"settings": {
"RequireIfMatch": true,
"VerbosePatchSupported": true,
"PrimaryEnforcement": "reject"
}
}
}'121 route handlers across 33 controllers. Full reference with request/response examples: docs/COMPLETE_API_REFERENCE.md
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
Public | Health check |
| GET | /scim/admin/version |
Bearer | Server version, runtime info, deployment metadata |
| Method | Path | Description |
|---|---|---|
| POST | /scim/admin/endpoints |
Create endpoint |
| GET | /scim/admin/endpoints |
List endpoints (?active=true&view=summary) |
| GET | /scim/admin/endpoints/:id |
Get endpoint by ID |
| GET | /scim/admin/endpoints/by-name/:name |
Get endpoint by name |
| PATCH | /scim/admin/endpoints/:id |
Update endpoint |
| DELETE | /scim/admin/endpoints/:id |
Delete endpoint (cascade) |
| GET | /scim/admin/endpoints/:id/stats |
Endpoint resource statistics |
| GET | /scim/admin/endpoints/presets |
List available presets |
| GET | /scim/admin/endpoints/presets/:name |
Get preset details |
| Method | Path | Description |
|---|---|---|
| POST | /scim/admin/endpoints/:id/credentials |
Create per-endpoint credential |
| GET | /scim/admin/endpoints/:id/credentials |
List credentials |
| DELETE | /scim/admin/endpoints/:id/credentials/:credId |
Revoke credential |
| Method | Path | Description |
|---|---|---|
| GET | /scim/admin/logs |
Paginated audit trail |
| GET | /scim/admin/logs/:id |
Single log entry |
| POST | /scim/admin/logs/clear |
Clear all logs |
| POST | /scim/admin/logs/prune |
Prune old logs |
| GET | /scim/admin/log-config |
Get log configuration |
| PUT | /scim/admin/log-config |
Update log configuration |
| PUT | /scim/admin/log-config/level/:level |
Set global log level |
| PUT | /scim/admin/log-config/category/:cat/:level |
Set category log level |
| PUT | /scim/admin/log-config/endpoint/:id/:level |
Set endpoint log level |
| DELETE | /scim/admin/log-config/endpoint/:id |
Reset endpoint log level |
| GET | /scim/admin/log-config/recent |
Query ring buffer |
| GET | /scim/admin/log-config/audit |
Audit log entries |
| DELETE | /scim/admin/log-config/recent |
Clear ring buffer |
| GET | /scim/admin/log-config/stream |
SSE live log stream |
| GET | /scim/admin/log-config/download |
Download logs (NDJSON/JSON) |
| GET | /scim/admin/log-config/prune |
Auto-prune config |
| PUT | /scim/admin/log-config/prune |
Update auto-prune config |
| GET | /scim/endpoints/:id/logs/recent |
Endpoint-scoped ring buffer |
| GET | /scim/endpoints/:id/logs/stream |
Endpoint-scoped SSE stream |
| GET | /scim/endpoints/:id/logs/download |
Endpoint-scoped log download |
| GET | /scim/endpoints/:id/logs/history |
Endpoint-scoped persistent log history |
| Method | Path | Description |
|---|---|---|
| GET | /scim/admin/database/users |
Database user browser |
| GET | /scim/admin/database/groups |
Database group browser |
| GET | /scim/admin/database/users/:id |
User detail |
| GET | /scim/admin/database/groups/:id |
Group detail |
| GET | /scim/admin/database/statistics |
Database statistics |
| GET | /scim/admin/activity |
Activity feed |
| GET | /scim/admin/activity/summary |
Activity summary |
| Method | Path | Description |
|---|---|---|
| POST | /scim/admin/users/manual |
Create user via simplified form |
| POST | /scim/admin/groups/manual |
Create group via simplified form |
| POST | /scim/admin/users/:id/delete |
Delete user via admin API |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /scim/Schemas |
Public | Global schemas (RFC 7644 S4) |
| GET | /scim/Schemas/:uri |
Public | Schema by URN |
| GET | /scim/ResourceTypes |
Public | Global resource types |
| GET | /scim/ResourceTypes/:id |
Public | Resource type by ID |
| GET | /scim/ServiceProviderConfig |
Public | Global SPC |
| GET | /scim/endpoints/:id/Schemas |
Public | Endpoint-scoped schemas |
| GET | /scim/endpoints/:id/ResourceTypes |
Public | Endpoint-scoped resource types |
| GET | /scim/endpoints/:id/ServiceProviderConfig |
Public | Endpoint-scoped SPC |
| Method | Path | Description |
|---|---|---|
| POST | /scim/endpoints/:id/Users |
Create user |
| GET | /scim/endpoints/:id/Users |
List/filter users |
| GET | /scim/endpoints/:id/Users/:uid |
Get user |
| PUT | /scim/endpoints/:id/Users/:uid |
Replace user |
| PATCH | /scim/endpoints/:id/Users/:uid |
Modify user |
| DELETE | /scim/endpoints/:id/Users/:uid |
Delete user |
| POST | /scim/endpoints/:id/Users/.search |
Search users (POST) |
| Method | Path | Description |
|---|---|---|
| POST | /scim/endpoints/:id/Groups |
Create group |
| GET | /scim/endpoints/:id/Groups |
List/filter groups |
| GET | /scim/endpoints/:id/Groups/:gid |
Get group |
| PUT | /scim/endpoints/:id/Groups/:gid |
Replace group |
| PATCH | /scim/endpoints/:id/Groups/:gid |
Modify group |
| DELETE | /scim/endpoints/:id/Groups/:gid |
Delete group |
| POST | /scim/endpoints/:id/Groups/.search |
Search groups (POST) |
| Method | Path | Description |
|---|---|---|
| POST | /scim/endpoints/:id/Bulk |
Bulk operations (RFC 7644 S3.7) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /scim/endpoints/:id/Me |
OAuth | Get current user |
| PUT | /scim/endpoints/:id/Me |
OAuth | Replace current user |
| PATCH | /scim/endpoints/:id/Me |
OAuth | Modify current user |
| DELETE | /scim/endpoints/:id/Me |
OAuth | Delete current user |
| Method | Path | Description |
|---|---|---|
| POST | /scim/endpoints/:id/:resourceType |
Create custom resource |
| GET | /scim/endpoints/:id/:resourceType |
List custom resources |
| GET | /scim/endpoints/:id/:resourceType/:rid |
Get custom resource |
| PUT | /scim/endpoints/:id/:resourceType/:rid |
Replace custom resource |
| PATCH | /scim/endpoints/:id/:resourceType/:rid |
Modify custom resource |
| DELETE | /scim/endpoints/:id/:resourceType/:rid |
Delete custom resource |
| POST | /scim/endpoints/:id/:resourceType/.search |
Search custom resources |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /scim/oauth/token |
Public | Exchange credentials for JWT |
| GET | /scim/oauth/test |
Public | OAuth service health |
POST /scim/endpoints/{endpointId}/Users HTTP/1.1
Host: localhost:8080
Authorization: Bearer changeme-scim
Content-Type: application/scim+json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"userName": "jane.doe@example.com",
"name": {
"givenName": "Jane",
"familyName": "Doe"
},
"displayName": "Jane Doe",
"emails": [
{ "value": "jane.doe@example.com", "type": "work", "primary": true }
],
"active": true,
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"department": "Engineering",
"costCenter": "CC-1234"
}
}Response (201 Created):
HTTP/1.1 201 Created
Content-Type: application/scim+json; charset=utf-8
Location: http://localhost:8080/scim/v2/endpoints/{endpointId}/Users/f47ac10b-...
ETag: W/"1"
X-Request-Id: 550e8400-...
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"userName": "jane.doe@example.com",
"name": {
"givenName": "Jane",
"familyName": "Doe"
},
"displayName": "Jane Doe",
"emails": [
{ "value": "jane.doe@example.com", "type": "work", "primary": true }
],
"active": true,
"externalId": null,
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"department": "Engineering",
"costCenter": "CC-1234"
},
"meta": {
"resourceType": "User",
"created": "2026-04-24T10:00:00.000Z",
"lastModified": "2026-04-24T10:00:00.000Z",
"location": "http://localhost:8080/scim/v2/endpoints/{endpointId}/Users/f47ac10b-...",
"version": "W/\"1\""
}
}PATCH /scim/endpoints/{endpointId}/Users/{userId} HTTP/1.1
Host: localhost:8080
Authorization: Bearer changeme-scim
Content-Type: application/scim+json
If-Match: W/"1"
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "displayName",
"value": "Jane D. Smith"
},
{
"op": "add",
"path": "emails[type eq \"home\"].value",
"value": "jane.home@example.com"
},
{
"op": "replace",
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department",
"value": "Product"
},
{
"op": "remove",
"path": "phoneNumbers[type eq \"fax\"]"
}
]
}Response (200 OK):
POST /scim/endpoints/{endpointId}/Groups HTTP/1.1
Host: localhost:8080
Authorization: Bearer changeme-scim
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "Engineering Team",
"members": [
{ "value": "f47ac10b-..." },
{ "value": "a83bc20e-..." }
]
}GET /scim/endpoints/{endpointId}/Users?filter=userName%20sw%20%22jane%22&startIndex=1&count=10&sortBy=userName&sortOrder=ascending&attributes=userName,displayName,emails HTTP/1.1
Host: localhost:8080
Authorization: Bearer changeme-scimResponse (200 OK):
// Schematic response: shortened identifiers and locations.
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "f47ac10b-...",
"userName": "jane.doe@example.com",
"displayName": "Jane D. Smith",
"emails": [
{
"value": "jane.doe@example.com",
"type": "work",
"primary": true
}
],
"meta": {
"resourceType": "User",
"created": "2026-04-24T10:00:00.000Z",
"lastModified": "2026-04-24T10:05:00.000Z",
"location": "...",
"version": "W/\"2\""
}
}
]
}// Schematic response: shortened identifiers and diagnostic paths.
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:Error"
],
"status": "409",
"scimType": "uniqueness",
"detail": "User with userName 'jane.doe@example.com' already exists",
"urn:scimserver:api:messages:2.0:Diagnostics": {
"requestId": "550e8400-...",
"endpointId": "a1b2c3d4-...",
"logsUrl": "/scim/endpoints/a1b2c3d4-.../logs/recent?requestId=550e8400-...",
"conflictingResourceId": "f47ac10b-...",
"conflictingAttribute": "userName",
"incomingValue": "jane.doe@example.com"
}
}Full RFC 7644 S3.4.2.2 filter grammar with database push-down optimization.
| Operator | Example | Description |
|---|---|---|
eq |
userName eq "jane" |
Equal (case-insensitive for strings) |
ne |
active ne true |
Not equal |
co |
displayName co "Smith" |
Contains |
sw |
userName sw "jane" |
Starts with |
ew |
emails.value ew "@example.com" |
Ends with |
gt |
meta.lastModified gt "2026-01-01" |
Greater than |
ge |
meta.created ge "2026-01-01" |
Greater than or equal |
lt |
meta.lastModified lt "2026-12-31" |
Less than |
le |
meta.created le "2026-12-31" |
Less than or equal |
pr |
externalId pr |
Present (not null/empty) |
filter=userName sw "jane" and active eq true
filter=displayName co "Smith" or displayName co "Jones"
filter=not (active eq false)
filter=emails[type eq "work"].value co "@example.com"
RFC 7644 S3.4.2.5 - control which attributes appear in responses.
| Parameter | Effect |
|---|---|
attributes=userName,emails |
Return ONLY these attributes (plus always-returned: schemas, id, meta) |
excludedAttributes=phoneNumbers,addresses |
Return all DEFAULT attributes EXCEPT these |
| Value | Behavior |
|---|---|
always |
Always included (schemas, id, meta, userName for Users, displayName for Groups) |
default |
Included by default, removable via excludedAttributes |
request |
Only included when explicitly listed in attributes |
never |
Never returned in any response (e.g., password) |
RFC 7644 S3.7 - process multiple operations in a single request.
POST /scim/endpoints/{endpointId}/Bulk HTTP/1.1
Host: localhost:8080
Authorization: Bearer changeme-scim
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"],
"failOnErrors": 2,
"Operations": [
{
"method": "POST",
"path": "/Users",
"bulkId": "user1",
"data": {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "bulk.user@example.com",
"displayName": "Bulk User"
}
},
{
"method": "POST",
"path": "/Groups",
"bulkId": "group1",
"data": {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "Bulk Group",
"members": [{ "value": "bulkId:user1" }]
}
}
]
}Response (200 OK):
// Schematic response: shortened identifiers and locations.
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:BulkResponse"
],
"Operations": [
{
"method": "POST",
"bulkId": "user1",
"status": "201",
"location": ".../Users/f47ac10b-...",
"response": {
"id": "f47ac10b-...",
"userName": "bulk.user@example.com"
}
},
{
"method": "POST",
"bulkId": "group1",
"status": "201",
"location": ".../Groups/b23cd40e-...",
"response": {
"id": "b23cd40e-...",
"displayName": "Bulk Group"
}
}
]
}Limits: Max 1,000 operations per request, 1 MB payload size. Requires bulk.supported: true in endpoint ServiceProviderConfig (enabled in rfc-standard preset).
flowchart TB
subgraph Sources
R[Request Interceptor]
S[Services]
G[Guards]
end
subgraph ScimLogger
RB[Ring Buffer<br>In-memory, 1000 entries]
FW[File Writer<br>Rotating, per-endpoint]
SSE[SSE Emitter<br>Live stream]
end
subgraph Persistence
DB[(RequestLog Table)]
end
R --> RB
R --> FW
R --> SSE
R --> DB
S --> RB
S --> FW
S --> SSE
# Live log stream (SSE)
curl -N http://localhost:8080/scim/admin/log-config/stream \
-H "Authorization: Bearer changeme-scim"
# Endpoint-scoped stream
curl -N http://localhost:8080/scim/endpoints/{id}/logs/stream \
-H "Authorization: Bearer changeme-scim"
# Query ring buffer
curl "http://localhost:8080/scim/admin/log-config/recent?limit=50&level=ERROR" \
-H "Authorization: Bearer changeme-scim"
# Download logs as NDJSON
curl http://localhost:8080/scim/admin/log-config/download?format=ndjson \
-H "Authorization: Bearer changeme-scim" -o logs.ndjson
# Change global level at runtime
curl -X PUT http://localhost:8080/scim/admin/log-config/level/DEBUG \
-H "Authorization: Bearer changeme-scim"
# Set endpoint-specific level
curl -X PUT http://localhost:8080/scim/admin/log-config/endpoint/{id}/TRACE \
-H "Authorization: Bearer changeme-scim"
# Audit trail (persistent, paginated)
curl "http://localhost:8080/scim/admin/logs?page=1&pageSize=50&method=POST&status=201" \
-H "Authorization: Bearer changeme-scim"TRACE < DEBUG < INFO < WARN < ERROR < FATAL
- Ring buffer: In-memory circular buffer (configurable size) for low-latency log queries
- SSE live stream: Real-time log events via Server-Sent Events, filterable by level/category/endpoint
- Per-endpoint isolation: Each endpoint gets its own log stream and optional log file
- Rotating file writer: Configurable max size and file count
- Auto-prune: Scheduled cleanup of old RequestLog entries (configurable retention days)
- Request correlation: Every request gets a
X-Request-IdUUID, threaded through all log entries - Slow request detection: Configurable threshold (
LOG_SLOW_REQUEST_MS, default 2000ms)
A React + Vite single-page application served from the root app shell. Below is the live dashboard from the production instance:
| Screen | Description |
|---|---|
| Dashboard | KPI rollups, endpoint cards, activity analytics, health and notifications chrome |
| Endpoints | Create/edit/delete endpoints, preset picker, settings toggles, credentials, users, groups, bulk, resource types, schemas, logs, activity |
| Workbench | Free-form SCIM request builder with headers editor, path autocomplete, request/response layout toggle, history, curl/fetch/Insomnia/Postman export |
| Discovery | Endpoint-scoped Schemas/ResourceTypes/ServiceProviderConfig viewer with two-endpoint schema diff |
| Operations | Cross-endpoint users/groups/statistics view with export controls |
| Logs & Settings | Structured log viewer, live stream, log config admin, version/runtime cards, preferences, telemetry |
| Manual Provisioning & /Me | Operator forms for users/groups and OAuth /Me self-service profile surface |
Access at http://localhost:8080/ (no separate build step needed - pre-built in Docker image).
See the full walkthrough with per-page screenshots and API endpoint tables in the Web Admin UI Guide.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 (dev), 8080 (Docker) |
HTTP listen port |
API_PREFIX |
scim |
URL prefix for all routes |
PERSISTENCE_BACKEND |
prisma |
Storage backend: prisma (PostgreSQL) or inmemory |
DATABASE_URL |
postgresql://scim:scim@localhost:5432/scimdb |
PostgreSQL connection string |
NODE_ENV |
development |
development, test, or production |
REQUEST_TIMEOUT_MS |
120000 |
Server request timeout in milliseconds |
| Variable | Default | Description |
|---|---|---|
SCIM_SHARED_SECRET |
Auto-generated (dev) | Global bearer token. Required in production |
JWT_SECRET |
Auto-generated (dev) | JWT signing key. Required in production |
OAUTH_CLIENT_ID |
scimserver-client |
OAuth client identifier |
OAUTH_CLIENT_SECRET |
Auto-generated (dev) | OAuth client secret. Required in production |
OAUTH_CLIENT_SCOPES |
scim.read,scim.write,scim.manage |
Comma-separated allowed scopes |
CREDENTIAL_KEK |
changeme-credential-kek |
Key-encryption-key for the re-viewable-credential-secret feature (WI-6 active - envelope-encryption foundation). Must match across all instances/redeploys; not on the auth path; default is cosmetic until rotated. See docs/auth/CONNECTION_INFO_AND_ENTRA_SETUP.md section 6A |
JWKS_HOST_ALLOWLIST |
(empty = all rejected) | Comma-separated IdP hostnames SCIMServer may fetch JWKS from (anti-SSRF choke point). Required before WIF can validate an assertion; empty fails closed. e.g. login.microsoftonline.com. Forthcoming (WI-15): prepopulated well-known seed + persisted, admin-editable hot-reload layer. See docs/auth/CONNECTION_INFO_AND_ENTRA_SETUP.md section 5D |
JWKS_CACHE_MAX_AGE_MS |
600000 |
Max age (ms) of a cached JWKS before it is refetched. Server-level default; per-endpoint override JwksCacheMaxAgeMs. Bounds 0 - 86400000 |
JWKS_FETCH_TIMEOUT_MS |
5000 |
Per-attempt timeout (ms) for the runtime WIF JWKS fetch (a hung IdP is aborted). Server-level default; per-endpoint override JwksFetchTimeoutMs. Bounds 100 - 60000 |
JWKS_FETCH_RETRIES |
2 |
Retries for a failed runtime JWKS fetch (total tries = retries + 1). Server-level default; per-endpoint override JwksFetchRetries. Bounds 0 - 10 |
JWKS_FETCH_RETRY_BACKOFF_MS |
200 |
Base retry backoff (ms); exponential with jitter. Server-level default; per-endpoint override JwksFetchRetryBackoffMs. Bounds 0 - 10000 |
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO (prod) |
Global log level: TRACE/DEBUG/INFO/WARN/ERROR/FATAL |
LOG_FORMAT |
json (prod) / pretty (dev) |
Log output format |
LOG_INCLUDE_PAYLOADS |
false (prod) / true (dev) |
Include request/response bodies in logs |
LOG_INCLUDE_STACKS |
true |
Include stack traces in error logs |
LOG_MAX_PAYLOAD_SIZE |
8192 |
Max payload size in log entries (bytes) |
PERSIST_REQUEST_SECRETS |
true |
Server-level default for whether the RequestLog stores + displays the COMPLETE request/response (headers + body, secrets included) for fast RCA. Set false to redact secret-bearing values (Authorization, client_secret, access_token, ...) before persist/display. Per-endpoint override: the PersistRequestSecrets config flag (endpoint overrides server). Console/file structured logs always redact regardless. |
LOG_SLOW_REQUEST_MS |
2000 |
Slow request threshold (ms) |
LOG_CATEGORY_LEVELS |
(none) | Per-category level overrides |
LOG_RETENTION_DAYS |
30 |
Auto-prune retention period |
LOG_FILE |
(disabled) | Path to write log file |
LOG_FILE_MAX_SIZE |
10485760 |
Max log file size before rotation (10 MB) |
LOG_FILE_MAX_COUNT |
3 |
Max number of rotated log files |
| Variable | Default | Description |
|---|---|---|
APP_VERSION |
from package.json | Application version |
GIT_COMMIT |
(none) | Git commit SHA |
BUILD_TIME |
(none) | Build timestamp |
TZ |
UTC |
IANA timezone |
| Variable | Default | Description |
|---|---|---|
SCIM_RG |
(none) | Azure resource group |
SCIM_APP |
(none) | Azure Container App name |
SCIM_REGISTRY |
(none) | Azure Container Registry name |
CONTAINER_APP_NAME |
(none) | Container App environment variable |
| Layer | Observed result |
|---|---|
| Complete six-mode orchestrator | All six modes PASS, no skip flags |
| API units, each backend | 6,128 PASS, 211 suites |
| Full HTTP, InMemory | 2,528 PASS, four PostgreSQL-only exclusions, 119 suites |
| Full HTTP, PostgreSQL | 2,532 PASS, 119 suites |
| Web units and coverage | 1,655 PASS, 124 files; lines 85.74%, branches 75.89%, functions 75.53%, statements 83.06% |
| Complete Node / Docker live suites | 1,811 PASS each, both targets report 0.55.57 |
| Shipping-image entrypoint | 24 PASS, zero FAIL |
| Complete Docker browser | 327 PASS, zero skipped/unexpected/flaky; unchanged pixel baselines |
| Existing static-analysis debt | API 0 errors / 513 warnings; 66 unchanged frontend TypeScript diagnostics |
| Dev, native Entra and customer deployment | Not performed by this isolated source consolidation |
The original stash and the other release cycle's resources are preserved. These local receipts do not renew older deployed-version evidence. Normal push, reviewed exact-tip CI and merge remain PR-controlled boundaries; see the release handoff.
| Layer | 0.55.54 result |
|---|---|
| API units, each backend mode | 6,060 PASS, 211 suites |
| Full HTTP, InMemory | 2,516 PASS, four PostgreSQL-specific exclusions, 119 suites |
| Full HTTP, PostgreSQL | 2,520 PASS, 119 suites |
| Default protected CI/pre-push HTTP | 2,460 PASS, four exclusions, 117 suites; two logging suites are included by the full release matrix instead |
| Web units | 1,624 PASS, 123 files |
| Docker/dev/canary live checks | 1,795 registered assertions per run, including embedded oracle self-checks |
| Native Entra provisioning | 50 checks across three auth modes; actual membership readback and 15 successful cleanup entries |
| Authoritative dev browser | 324 expected passes, zero skipped/unexpected/flaky |
These selections overlap; do not sum repeated runs as unique coverage. See release validation scope and limits.
cd api
# Unit tests
npx jest --no-coverage
# E2E tests
npx jest --config test/e2e/jest-e2e.config.ts --no-coverage --forceExit
# Live tests (local dev server must be running on port 6000)
cd ../scripts
pwsh ./live-test.ps1
# Live tests against Docker
pwsh ./live-test.ps1 -BaseUrl http://localhost:8080 -ClientSecret "changeme-oauth"
# Live tests against Azure
pwsh ./live-test.ps1 -BaseUrl https://myapp.azurecontainerapps.io -ClientSecret "your-secret"
# Lexmark ISV validator
pwsh ./lexmark-live-test.ps1
# Full validation pipeline (unit + E2E + lint)
pwsh ./full-validation-pipeline.ps1| Metric | Threshold |
|---|---|
| Branches | 75% |
| Functions | 90% |
| Lines | 80% |
| Statements | 80% |
| Component | Technology | Version |
|---|---|---|
| Runtime | Node.js | 24 |
| Framework | NestJS | 11.1 |
| Language | TypeScript | 5.9 |
| ORM | Prisma | 7.4 |
| Database | PostgreSQL | 17 |
| Extensions | citext, pgcrypto, pg_trgm | - |
| Testing | Jest | 30.2 |
| E2E Testing | Supertest | 7.2 |
| Frontend | React + Vite | 19 / 8 |
| Auth | Passport JWT, bcrypt | - |
| Linting | ESLint 10, Prettier 3.8 | - |
| Container | Docker (node:24-alpine) | - |
| Infrastructure | Azure Bicep | - |
SCIMServer/
+-- api/ # NestJS backend
| +-- src/
| | +-- main.ts # Application bootstrap
| | +-- modules/
| | | +-- app/ # Root module
| | | +-- auth/ # SharedSecretGuard (3-tier auth)
| | | +-- scim/ # SCIM protocol implementation
| | | | +-- controllers/ # 20 controller files (admin, SCIM, discovery)
| | | | +-- services/ # Users, Groups, Generic, Bulk services
| | | | +-- discovery/ # Schema registry, discovery service
| | | | +-- endpoint-profile/ # Profile engine (presets, expand, validate)
| | | | +-- filters/ # SCIM filter parser, exception filters
| | | | +-- interceptors/ # Content-Type, ETag interceptors
| | | | +-- middleware/ # Content-Type validation
| | | | +-- dto/ # Request/response DTOs
| | | | +-- utils/ # Attribute projection, sort, errors
| | | +-- endpoint/ # Endpoint CRUD & config management
| | | +-- database/ # Cross-endpoint operations controller
| | | +-- logging/ # Structured logging engine
| | | +-- health/ # Health check
| | | +-- prisma/ # Prisma service
| | | +-- web/ # Web UI serving
| | +-- domain/
| | | +-- patch/ # Pure PATCH engines (User, Group, Generic)
| | | +-- validation/ # Schema validator (10 validation types)
| | | +-- models/ # Domain models
| | | +-- repositories/ # Repository interfaces
| | | +-- errors/ # Domain errors
| | +-- infrastructure/
| | | +-- repositories/
| | | +-- prisma/ # PostgreSQL repositories
| | | +-- inmemory/ # In-memory repositories
| | +-- oauth/ # OAuth 2.0 token service
| +-- prisma/
| | +-- schema.prisma # Database schema (6 models)
| | +-- migrations/ # 12 migrations
| +-- test/
| +-- e2e/ # 63 E2E spec files + helpers
+-- web/ # React + Vite frontend
| +-- src/
| | +-- components/ # Shared UI primitives and feature surfaces
| +-- e2e/ # Playwright E2E tests
+-- scripts/ # DevOps tooling
| +-- live-test.ps1 # 1,027 Live SCIM assertions
| +-- lexmark-live-test.ps1 # ISV validator tests
| +-- deploy-azure.ps1 # Azure deployment script
| +-- full-validation-pipeline.ps1
+-- infra/ # Azure Bicep templates
+-- docs/ # 69+ documentation files
+-- Dockerfile # 4-stage production build
+-- docker-compose.yml # PostgreSQL + API
+-- deploy.ps1 # One-click Azure deployment
+-- setup.ps1 # Non-interactive Azure setup
+-- bootstrap.ps1 # Cache-busting bootstrap loader
Full documentation: docs/INDEX.md
| Document | Description |
|---|---|
| COMPLETE_API_REFERENCE.md | All 121 route handlers, full request/response examples |
| ENDPOINT_LIFECYCLE_AND_USAGE.md | Quick start - endpoint lifecycle recipes |
| ENDPOINT_PROFILE_ARCHITECTURE.md | Profile system - presets, expansion, validation |
| ENDPOINT_CONFIG_FLAGS_REFERENCE.md | All 16 configuration flags |
| SCHEMA_CUSTOMIZATION_GUIDE.md | Custom schemas and extensions guide |
| LOGGING_AND_OBSERVABILITY.md | Structured logging deep dive |
| AZURE_DEPLOYMENT_AND_USAGE_GUIDE.md | Azure Container Apps deployment |
| DOCKER_GUIDE_AND_TEST_REPORT.md | Docker build, run, and test guide |
| LOCAL_DOCKER_BUILD.md | Building the image behind a corporate network - the NPM_REGISTRY build arg, why npm ci hangs without it, and why the lockfile stays authoritative |
| SCIM_COMPLIANCE.md | RFC compliance matrix |
| G11_PER_ENDPOINT_CREDENTIALS.md | Per-endpoint authentication |
| TECHNICAL_DESIGN_DOCUMENT.md | Architecture deep dive |
- Fork the repository
- Create a feature branch
- Run the full test suite:
cd api && npx jest && npx jest --config test/e2e/jest-e2e.config.ts --forceExit - Follow existing patterns and coding conventions
- Submit a pull request
- Always use
git add -A; git commit -m "<descriptive message>" - Never use
git commit --amendunless explicitly requested - Never rewrite history on pushed commits
Released under the MIT License, as declared in api/package.json.
