* refactor(backend): extract EntitlementProvider abstraction (Phase 1)
Phase 1 of the open-core hybrid extraction described in
docs/internal/adrs/2026-05-02-open-core-hybrid-strategy.md. Introduces
the abstraction without moving any code out of the public repo; Phase
2 will actually move services/LicenseService.ts to a private
@studio-saelix/sencho-pro package.
The new backend/src/entitlements/ module contains:
- types.ts. The EntitlementProvider interface plus all tier/license
types (LicenseTier, LicenseVariant, LicenseInfo, SeatLimits,
ActivationResult, etc.). The interface mirrors the existing
LicenseService public surface so the migration was mechanical.
- registry.ts. Module-scope holder for the active provider with
setEntitlementProvider, getEntitlementProvider, and a test-only
reset helper. getEntitlementProvider throws if called before
bootstrap registers a provider; the throw is intentional fail-fast
on a bootstrap-order bug rather than a silent degradation.
- CommunityEntitlementProvider.ts. Phase 2 fallback that returns
community tier and rejects activate(). NOT instantiated in
production today; a smoke test keeps it covered against bitrot.
- loadProvider.ts. Async resolver. Phase 1 returns
LicenseService.getInstance() directly. The async signature matches
what Phase 2 needs (dynamic import of @studio-saelix/sencho-pro
with a "module not found" vs "construction threw" narrowing); the
call site does not change between phases.
- headers.ts. PROXY_TIER_HEADER and PROXY_VARIANT_HEADER constants.
These are part of the wire contract between Sencho instances and
belong in the public core regardless of which entitlement provider
is bound.
- normalize.ts. isLicenseTier, isLicenseVariant, normalizeTier,
normalizeVariant. Domain knowledge about Sencho's tier model
(legacy name maps from pre-0.38.1 versions), not LemonSqueezy
internals. Phase 2 keeps these in the public core.
services/LicenseService.ts now imports its types from
entitlements/types and adds an "implements EntitlementProvider"
clause. Re-exports the types for back-compat with ~20 type-only
consumers; a follow-up PR will sweep those imports to entitlements/
types directly before Phase 2 deletes the file.
bootstrap/startup.ts awaits loadEntitlementProvider, registers the
result, then calls initialize. shutdown.ts calls
getEntitlementProvider().destroy() instead of the LicenseService
singleton.
middleware/tierGates.ts, the chokepoint for ~154 tier-check call
sites, now reads through getEntitlementProvider. Sixteen other
production files (routes/{fleet,imageUpdates,license,permissions,
scheduledTasks,security,stacks,templates,users,webhooks},
services/{BlueprintService,CloudBackupService,SchedulerService,
SSOService}, proxy/remoteNodeProxy, websocket/{hostConsole,
remoteForwarder}, middleware/auth) had their LicenseService.getInstance
calls and utility-export imports redirected to the entitlements
module. The only remaining LicenseService.getInstance in production
code is in entitlements/loadProvider.ts itself, which is the
intentional Phase-1 binding site.
Test infrastructure: setupTestDb registers
LicenseService.getInstance() as the active provider so existing
test files using the helper need no changes. The mocking pattern
many tests use, vi.spyOn(LicenseService.getInstance(), 'getTier'),
keeps working because LicenseService.getInstance() and
getEntitlementProvider() return the same singleton in Phase 1.
scheduler-service.test.ts is the only test that does not use
setupTestDb but exercises tier-gating; it now mocks
entitlements/registry alongside its existing LicenseService mock.
Adds a smoke test for CommunityEntitlementProvider so the Phase 2
fallback class stays covered.
Adds an architecture doc at
docs/internal/architecture/entitlement-provider.md covering the
runtime registry, bootstrap order invariants, and the Phase 1 vs
Phase 2 binding table.
Test results: 89/89 backend test files pass, 1657 passing tests, 5
pre-existing skips. The pre-existing database-metrics > handles
1000+ metrics stress test continues to flake under parallel load
and pass when re-run solo, same flake observed in PRs #862, #863.
* chore(backend): drop unused entitlement type imports from LicenseService
Phase 1 of the EntitlementProvider extraction left five type imports
(ActivationResult, BillingPortalError, BillingPortalResult,
DeactivationResult, ValidationResult) unreferenced after the runtime
methods that produced them began inferring their result shapes via the
EntitlementProvider interface contract. ESLint's no-unused-vars rule
flagged them as errors and failed the lint step in CI.
Self-hosted Docker Compose management with self-healing infrastructure, atomic deployments, and multi-network fleet control. Built for engineers who want Kubernetes-grade reliability without the complexity.
Why Sencho?
Works across every network
Pilot Agent tunnels through NAT and firewalls so every node in your fleet is reachable from the dashboard, regardless of network topology. No port-forwarding. No VPN. No SSH.
Containers crash. Sencho fixes them.
Auto-heal policies watch your stacks and take action (restart, redeploy, notify) before you even open your laptop. Define the rules once; the recovery runs itself.
Deploy safely without Kubernetes
Atomic deployments mean a failed stack never leaves you in a broken half-up state. Rollback is one click. Deploy enforcement policies block non-compliant images before they ever run.
Define behavior, not procedures
Auto-update policies, scheduled operations, webhooks, and deploy enforcement let you codify how your infrastructure behaves. The UI is for oversight; the engine runs the work.
Features
Stack Management
- Monaco editor with syntax highlighting and multi-file support
- Atomic deployments with live progress tracking and one-click rollback
- Git-sourced stacks: pull and sync from any repository
- Stack file explorer and label management
Monitoring & Observability
- Live log streaming with search and filtering across all containers
- CPU, RAM, and disk threshold alerts with configurable actions
- Container stats, health checks, and image update notifications
- Global observability view across all nodes in your fleet
Fleet Management
- Multi-node via authenticated HTTP/WebSocket proxy (no SSH, no remote socket)
- Fleet view and sync for coordinated updates across nodes
- Pilot Agent for NAT traversal and firewall-crossing connections
- Node compatibility checks before deploying
Automation
- Auto-heal policies: detect and recover from container failures automatically
- Auto-update policies: roll out new image versions on your terms
- Scheduled operations with cron expressions
- Webhooks and deploy enforcement rules
Security
- SSO: custom OIDC (any spec-compliant provider), Google/GitHub/Okta presets, LDAP/Active Directory
- Two-factor authentication and RBAC with deployer, node-admin, and auditor roles
- Audit log, vulnerability scanning with VEX suppression, and private registry support
- Deploy enforcement to block non-compliant images
Operations
- App Store with one-click LinuxServer.io template deployments
- Sencho Cloud Backup for off-site stack archives
- Notification routing to Slack, Discord, email, and webhooks
- Global search across stacks and containers
Architecture
Sencho's architecture combines a Distributed API model with an optional Pilot Agent for complex networks.
- Distributed API: Remote nodes are managed by proxying authenticated HTTP/WebSocket requests to autonomous Sencho instances running on each server. No SSH. No remote Docker socket exposure. No polling agent. Each node speaks the same Sencho API, so the dashboard is just another client.
- Pilot Agent: For nodes behind NAT or strict firewalls, the Pilot Agent establishes secure outbound tunnels to the primary dashboard instance. This guarantees full fleet visibility and management without requiring port-forwarding, VPNs, or inbound network access.
Quick Start
Docker run (quickest):
docker run -d --name sencho \
-p 1852:1852 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v sencho_data:/app/data \
-e COMPOSE_DIR=/opt/docker \
saelix/sencho:latest
Docker Compose (recommended for production):
services:
sencho:
image: saelix/sencho:latest
container_name: sencho
restart: unless-stopped
ports:
- "1852:1852"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
# 1:1 Compose Path Rule: the host path MUST match the container path
- /opt/docker:/opt/docker
environment:
- COMPOSE_DIR=/opt/docker
- DATA_DIR=/app/data
docker compose up -d
Then open http://your-server:1852 and create your admin account.
See the full documentation for configuration details, multi-node setup, and more.
Documentation
Development
# Backend (Express + TypeScript)
cd backend && npm install && npm run dev
# Frontend (React + Vite)
cd frontend && npm install && npm run dev
The frontend dev server proxies /api requests to the backend on port 1852.
Contributing
See CONTRIBUTING.md for development setup and PR guidelines.
Security
See SECURITY.md for vulnerability reporting. Do not open public issues for security vulnerabilities.
License
Sencho is licensed under the Business Source License 1.1. Production use is allowed; the only restriction is offering Sencho as a competing hosted or managed service. On 2030-03-25, the license automatically converts to Apache 2.0.

