Website is genuinely composed by both API Host and JobHost. It delivers content management/public reads, contact submission, anonymous telemetry, Web Vitals, experiments, funnels, short links, analytics marts, sitemap generation, and selected privacy endpoints. It does not include a website frontend, and consent, lead management, and scheduled publishing are not complete.
1. Architecture
Handwritten endpoints handle public workflows that need HTTP context; seven growth-management requests use [GenerateEndpoint]. API Host registers analytics, GeoIP, short links, cache invalidation, and persistence. JobHost runs mart and sitemap work.
2. Code map
| Project/location | Current responsibility |
|---|---|
| Contracts | DTOs, public ports, permissions, features, integration events |
| Domain | Content, Lead, AnalyticsEvent, Experiment, Funnel, ShortLink, mart rows |
| Application | Content/contact/growth handlers and job executors |
| Infrastructure | Stores, sanitizer, queue/DLQ, journey, GeoIP, protection, sitemap |
| API Host | Public/admin endpoints, visitor middleware, output cache, composition |
| JobHost | Quartz adapters, mart rebuild, sitemap submission consumers |
3. Capability matrix
| Area | Evidence in source | Not currently guaranteed |
|---|---|---|
| Content | Draft/update/schedule/publish/unpublish/archive, public query/feed/sitemap | Automatic due publishing, versions, approval, preview token |
| Contact | CAPTCHA, field protection, save, notification | Idempotency, notification reconciliation, lead-management API |
| Analytics | Bounded in-memory queue, batch store, mart, RUM, funnel, experiment | Consent, crash-safe intake, durable DLQ, exactly once |
| Privacy | Subject/visitor reads and soft deletion | Anonymous ownership proof, consent record, statutory workflow evidence |
| Short links | HTTP(S), random/custom code, expiry, cache, QR | Destination policy, revoke/update, tenant ownership, click availability |
| Governance | Six permissions, five default-off features | Runtime feature enforcement |
4. HTTP surface
Handwritten routes include /api/public/content*, /api/public/contact*, sitemap, /api/telemetry/*, public assignment, /r/{code}, and self-service privacy. Content management explicitly requires website.content.read/manage.
Generated growth routes derive website.growth.manage or website.analytics.read through IAuthorizedRequest; those match the catalog. Features have no equivalent execution evidence.
5. Public and tenant model
Content, Lead, and ShortLink have no TenantId and are platform-global. Analytics, Experiment, Funnel, and mart rows carry TenantId. Public analytics gets Website:PublicTenantId, defaulting to literal public.
Authenticated analytics uses ICurrentTenant. Internal assignment/privacy messages still carry tenant IDs, but host endpoints construct them from server configuration or trusted identity; do not expose those messages directly.
6. Actual telemetry example
The current path does not query consent. It builds context from middleware-owned visitor/session and coarse device/region fields.
// ① Visitor, session, and public tenant are server-derived.AnalyticsCaptureContext context = WithPublicTenant( AnonymousVisitorContextMiddleware.BuildCaptureContext(httpContext, visitor), configuration);
// ② The handler limits 1..50 and rejects forged experiment-assigned events.Result result = await mediator.Send( new CaptureTelemetryBatchCommand(context, body.Events), httpContext.RequestAborted);
// ③ Success means accepted by this process, not persisted.return result.ToHttp(httpContext);7. Protection is not a privacy system
Lead name/contact/inquiry use ASP.NET Core Data Protection. Analytics omits full IP/user-agent, strips URL query/fragment, and validates custom dimensions against a low-cardinality schema and heuristic PII checks.
These are minimization measures, not lawful basis, consent, withdrawal, retention, or subject verification. Visitor Journey remains in Redis for 400 days. There is no consent aggregate or retention job.
8. Reliability semantics
Analytics intake uses an in-process Channel. Full queue returns AnalyticsQueueFull; graceful stop drains; process crash loses queued data. Exhausted writes enter a per-instance ConcurrentDictionary DLQ that also disappears on restart.
If click telemetry is full, short-link redirect fails. If contact notification throws after saving, the request can fail after a lead already exists. These are current product semantics.
9. Governance gaps
Five default-off features—public content, contact, analytics, experiments, and short links—are declared but not consumed in runtime paths. Deployment cannot use them to close routes.
LeadRead/LeadManage and domain transitions exist, but no lead list/detail/decrypt/assignment/follow-up handler or endpoint exists. The catalog is not a delivered CRM.
10. Deep dives
- Content lifecycle, cache, and SEO
- Contact forms, leads, and sensitive data
- Analytics ingestion, anonymous identity, and privacy
- Experiments, funnels, and short links
- Jobs, testing, operations, and GA
11. Source checks
# ① Inspect handwritten and generated routes.rg -n "Map(Get|Post|Put|Delete)|GenerateEndpoint" \ src/Hosts/BitzOrcas.Api/Endpoints/Website* src/Platform/Website -g '*.cs'# ② Feature usage should currently remain catalog-only.rg -n "WebsiteFeatures\." src tests -g '*.cs' --glob '!**/bin/**' --glob '!**/obj/**'# ③ Scheduled has no due-content publisher.rg -n "PublishStatus\.Scheduled|ScheduleContent|Scheduled" src/Platform/Website src/Hosts -g '*.cs'12. Reading method
Start at endpoint authentication, authorization, rate limiting, and body limits; follow trusted-context construction into handlers and aggregate invariants; then inspect store transaction/tenant predicates, cache/events/jobs/failure recovery, and tests. A DTO or XML comment alone is not delivery evidence.
13. Change-review questions
- Does a new public route pass through visitor middleware and auto-pageview?
- Is the resource platform-global or tenant-owned?
- Does its feature actually execute in the request path?
- Is an anonymous identifier being mistaken for authorization?
- Does success mean accepted, committed, or external effect complete?
- Is state/event atomicity proven with an outbox?
- What happens to Redis, Channel, and DLQ state after restart?
- Can the Data Protection key ring be restored?
- Are HTML, URL, XML, and SVG outputs context-safe?
- Does erase cover queues, DLQ, marts, caches, and backup expiry?
- Do jobs define lease, duplication, and partial success?
- Do tests cover both ORMs, multiple instances, and real Host routes?