Skip to content

Architecture and authority boundaries

BloqueApp combines an Android enforcement client, an ASP.NET Core service, a SQLite state store, and a small browser app for the person who approves requests. Each component has a deliberately narrow role: the service authenticates and records decisions, while the phone applies verified policy locally.

For plain-language workflows, start with the user guide. The shared concepts explain policy, allowances, requests, sleep rules, and offline behavior. Terms are collected in the glossary; current boundaries are in limitations.

flowchart LR
  Phone[Android phone<br/>local policy and enforcement]
  API[ASP.NET Core API]
  DB[(SQLite)]
  Page[Approver page]
  SW[Browser service worker]
  Push[Web Push service]
  Hub[Authenticated SignalR hub]

  Phone <-->|HTTPS: policy, requests, receipts| API
  API <--> DB
  Page <-->|same-origin HTTPS: decisions and receipts| API
  API -->|durable delivery outbox| Push
  Push --> SW
  SW -->|open or focus page| Page
  API -->|best-effort change hint| Hub
  Hub -->|foreground phone connection| Phone

The diagram shows communication paths, not interchangeable authority. In particular, push and SignalR are delivery hints. SQLite records and signed policy or grant data carry the durable state.

Android client

The Android app is native Java with platform Views. Its local AccessPolicy evaluator interprets an accepted policy snapshot, package membership, sleep windows, and an active allowance. The RuntimePolicyEnforcer reconciles that decision with Android package suspension and scheduled expiry. Local evaluation does not call the server for every app transition.

The runtime policy snapshot is signed by the service and bound to the paired device and pairing. The client verifies the signature using a public key supplied as a build trust input, validates the bounded policy document, and stores the accepted envelope atomically. The enforcement state and policy cache use device-protected storage so reconciliation can run before the first unlock after reboot. Pairing credentials use Android Keystore-backed encryption in credential-protected storage.

Package suspension is an operating-system package-level action requiring Device Owner authority. It can prevent a selected package from launching; it does not identify or block individual tabs, videos, or features inside another package. Essential system and recovery packages are excluded from policy selection. The app keeps recovery and permission routes available.

Time banks are another local path. They use Android Usage Events to estimate eligible foreground intervals, an explicit user-started session, and a short renewable access lease. An uncertain sample ends the session without charging the uncertain interval. See bank credit and the algorithm analysis below.

Service and approver page

The service targets .NET 10 and uses ASP.NET Core Minimal APIs, Microsoft.Data.Sqlite, and SignalR. SQLite persists pairings, device and approver session hashes, approval requests and decisions, signed grants, runtime policy revisions, push subscriptions, and the Web Push outbox. Decision writes run in a SQLite transaction before a live hint is sent.

The approver is a same-origin static web app made from HTML, CSS, JavaScript, a manifest, and a service worker. A secure, HttpOnly, SameSite=Strict cookie identifies the approver session. When Web Push delivery is configured, the server queues a durable outbox item for each eligible subscription. The service worker can display the notification while the page is closed; an action must still reach the authenticated server and receive acknowledgement before the page presents it as complete.

SignalR is a foreground transport for the Android request screen. The hub accepts a paired device bearer credential, scopes connections to that device, and sends versioned RequestChanged hints. The phone responds by fetching the durable receipt over HTTPS. Resume and retry recovery also work without a SignalR message. A notification or hint never approves a request.

Deployment topology

Policy proposals use the same durable-state principle. The Android proposal screen receives PolicyProposalChanged hints on the device hub and retrieves receipts; acceptance then verifies the current signed policy. Open approver pages use a separate cookie-authenticated hub at /api/v1/approver/events for pairing-scoped workflow hints. A separate durable proposal outbox uses the existing browser Web Push sender for closed-page alerts that open authenticated review. These notifications cannot publish a policy themselves, and native Android background result delivery remains deferred.

In the production Compose shape, one ASP.NET Core container serves both the API and the static approver app. Kestrel listens on container port 8080; Compose maps it to a host-loopback port, 127.0.0.1:8090 by default. Host Nginx terminates public HTTPS on port 443 and proxies ordinary HTTP requests to that loopback mapping. The Android realtime endpoint uses the same public HTTPS origin with a WebSocket upgrade (wss://<public-host>/api/v1/device/events); Nginx forwards the upgrade to the same Kestrel service. This topology needs no public Kestrel port or separate WebSocket server. In local development Compose maps 127.0.0.1:5080 to container 8080 instead.

Forwarded scheme and client-address headers are trusted only when the actual proxy address seen by Kestrel is configured as a known proxy. The host port and proxy address are deployment settings, not portable guarantees. Readiness at /health/ready checks service storage dependencies; it does not demonstrate that a request was approved, a grant is valid on a phone, a push reached a browser, or Android enforcement is active. See security for trust boundaries and the user guide for what people experience.

Authority boundaries

The service owns device and approver authentication, pairing relationships, durable request decisions, source policy publication, and signing. It checks that a request belongs to the authenticated device and current pairing, that the requested package and duration are allowed by the current policy, and that a decision is made by the approver attached to that pairing.

The phone owns the final local enforcement decision. It verifies a grant against its pinned trust configuration and the expected issuer, device, pairing, request, package, policy identifier and revision, scope, and expiry. A valid signature proves that the trusted server issued those claims; it does not bypass the current local policy. The phone applies package suspension only after verification and local checks succeed.

The policy source and effective policy for a pairing have separate revisions. A narrow permanent-app exception can be recorded for a pairing, but it is not a general policy editor and cannot replace arbitrary source policy. A later explicit source restriction removes a matching exception. The phone uses the current signed effective policy when it evaluates local access.

The browser owns the approver interaction, not device access. Its cookie cannot authenticate as the Android device, and device credentials are not sent to the approver page. This separation means that the phone can enforce the last accepted policy locally while the browser and service are unavailable, but a new partner approval cannot be created offline.

Offline behavior and tradeoffs

When connectivity is absent, the phone can continue to evaluate a previously accepted signed policy and reconcile its local package state and deadlines. It cannot fetch a new policy or obtain a new signed partner allowance. An already accepted allowance keeps its absolute expiry across a policy update, while current package membership and sleep rules still apply. A policy refresh or pairing revocation is observed when the phone reconnects; offline operation cannot learn about a change that has not arrived.

The service remains the authority for durable decisions. The phone’s saved receipt is useful for recovering from a lost response, but it is not itself permission to open an app. If a receipt fetch fails, the client retries within bounded foreground recovery rules; it does not resubmit a different request or infer approval from a notification.

The choice favors local enforcement and bounded recovery over a network dependency in an Android event callback. Its limit is that remote revocation cannot take effect on a disconnected phone until the phone receives it; signed grants therefore have finite expiries, and the client applies known local schedule and policy restrictions immediately.

Scope

The implemented enforcement path is package-level: it cannot control individual browser tabs or content inside an app, and browser/domain controls are not provided. Enforcement also depends on actual Device Owner provisioning and the package-suspension capabilities available on a given Android version and device. Do not read service status flags or a running service as proof that a strict mode is active. For current behavior and boundaries, see limitations and the user guide.