Repository documentation
Adaptive app architecture
How verified contracts, semantic inference and reusable native components turn app APIs into useful interfaces.
On this page
This document defines the durable boundaries for integrating independently versioned Nextcloud server apps without embedding their web interfaces. It is for contributors changing transport, discovery, repositories, caching, sync, or native feature adapters.
Last reviewed: 2026-08-20. Architecture rules may have changed. The default-branch document is the source of truth for the maintained contract.
Current product status belongs in COMPATIBILITY.md. Planned delivery belongs in ROADMAP.md. This document contains rules that must remain true as those documents change.
Core invariant
The native UI consumes stable, typed product models. It does not parse protocol payloads, build endpoint URLs, infer permissions from navigation, or execute an operation that lacks verified provenance.
Compose UI
|
Feature state holders and repositories
|
Typed adapters and semantic runtime
|
Authenticated transport and persistence
|
Platform services
Each layer owns one kind of change. A server API change should not force a Compose screen to parse new JSON. A platform credential-store change should not alter repository policy. A layout change should not alter mutation safety.
Ownership boundaries
Compose UI
- Renders immutable state and sends explicit user intents.
- Owns presentation state, focus, accessibility semantics, and adaptive layout.
- Does not own network requests, protocol parsing, persistence, retry loops, or conflict policy.
- Reuses semantic components for shared interaction patterns. App-specific UI is justified only by behavior that cannot be represented safely by a shared component.
- Keeps composables small enough to review. Extract state holders, pure models, and reusable surfaces before adding another independent responsibility to a large screen.
Repositories and feature state
- Provide the single source of truth for cached and remote feature state.
- Own refresh, pagination, ETags, dirty state, retries, conflicts, and cache invalidation.
- Expose typed loading, ready, stale, partial-failure, and blocking-failure states instead of throwing protocol exceptions into the UI.
- Merge successful remote responses transactionally.
- Keep usable cached content visible when a refresh fails.
Adapters
- Translate one verified protocol or app-version family into shared models.
- Remain stateless apart from immutable capability and version configuration.
- Validate required capabilities, versions, endpoint paths, permissions, and response shapes before enabling an action.
- Preserve unknown response fields only through an explicit typed extension
value. Do not use
Anyas a compatibility strategy. - Fall back to a supported generic adapter when an optimized app-specific path is unavailable. Never fall back to a hidden web view.
Transport
- Owns authentication, product identification, TLS, redirect policy, bounded bodies, case-insensitive response headers, and same-origin enforcement.
- Accepts typed request data from adapters. The UI never constructs requests.
- Supports standard HTTP and required WebDAV methods without placing DAV parsing in platform launchers.
- Rejects DTDs and external entities in XML.
- Allows an external origin only through a feature designed as an explicit browser or application handoff.
Persistence and sync
- Scope every record, cache key, queued operation, and diagnostic identifier to an opaque local account ID.
- Keep credential material out of metadata databases and diagnostics.
- Publish files atomically after complete writes.
- Preserve originals unless the user explicitly chooses replacement.
- Bound automatic caches; keep offline files and unresolved conflict copies durable until their documented lifecycle permits removal.
- Queue only operations with defined idempotency and conflict behavior.
Platform services
Platform source sets own operating-system behavior: credentials, lifecycle, background scheduling, filesystem providers, notifications, media sessions, external handoff, packaging, and accessibility integration. Protocol policy and parsing belong in shared code even when execution uses a platform client.
NextcloudPlatformServices is an integration boundary, not a place to collect
every product feature. When Android and desktop implementations repeat protocol
logic, extract a shared adapter or repository. Keep separate implementations
when lifecycle or operating-system semantics are genuinely different.
Capability and discovery rules
Capabilities and versioned API descriptions are authoritative. Navigation entries and successful guesses are not proof that an operation is safe.
- Preserve a typed capability snapshot per account with its fetch time and server/app versions.
- Revalidate cached descriptors when the server version, app version, capability fingerprint, OpenAPI fingerprint, or adapter version changes.
- Treat response-shape inference as read-only evidence.
- Require advertised OpenAPI or a reviewed adapter for writes.
- Keep dynamic endpoints relative and inside approved same-origin prefixes.
- Omit behavior whose provenance or permission model is ambiguous.
See DYNAMIC_APP_DESCRIPTOR.md and NATIVE_SCHEMA.md for the serialized trust boundaries.
Mutation policy
Every operation declares its risk before it reaches the UI:
| Level | Meaning | Required behavior |
|---|---|---|
| Read | No intended remote mutation | May run for loading or explicit refresh. |
| Reversible | Small, visible, reversible write | Direct user intent and rollback on failure. |
| Guarded | Content change with concurrency risk | Permission check, revision guard, conflict UI. |
| Destructive | Delete, overwrite, or hard-to-reverse change | Target-specific confirmation and no blind background retry. |
| Privileged handoff | Server administration or primary-password confirmation | Explain the effect and open authenticated server administration. |
Runtime evidence may raise the risk level, such as when a move would overwrite an existing target. It must never lower the declared level silently.
Stored Login Flow app passwords must not be treated as primary passwords for strict administrator confirmation. The client must not collect or retain a primary account password to bypass that boundary.
Error and cancellation rules
Errors must retain enough structured context to support recovery and safe diagnostics without exposing private data.
- Map transport, authentication, permission, validation, conflict, capacity, cancellation, and unexpected failures into distinct typed outcomes.
- Never convert coroutine cancellation into an ordinary failed request. Rethrow cancellation before broad exception handling.
- Do not use
getOrNull()or an empty catch when the caller must distinguish unavailable data from a failed operation. - Add operation and stage identifiers at subsystem boundaries. Do not include server URLs, paths, filenames, payloads, credentials, or response bodies.
- Preserve the original cause internally while presenting an actionable, non-technical message to the user.
- A retry must be bounded and safe for the operation. Ambiguous delivery of a mutation requires reconciliation before another submission.
Offline and conflict rules
Repositories use stale-while-revalidate behavior:
- Emit usable cached data immediately.
- Mark it as refreshing when remote work starts.
- Commit a successful response transactionally.
- Keep cached data and expose a non-blocking error when refresh fails.
- Show a blocking error only when no usable state exists.
Writes require an explicit conflict contract. ETag-protected text or note saves may be queued when their base revision and payload are durable. Deletes, administrator actions, Talk messages, and hard-to-reverse recognition changes must not be queued by default.
Notes mutation validators preserve valid quoted opaque ETags verbatim, including
backslashes and HTTP obs-text bytes. They do not apply quoted-string unescaping.
Bare Notes API validators are quoted once; malformed and oversized validators
are rejected before a request. The character grammar follows
HTTP entity-tags.
Test contract
Each boundary has a corresponding test responsibility:
- Protocol fixtures cover parsing, omitted and unknown fields, version gates, same-origin checks, size limits, and hostile XML.
- Repository tests cover cache refresh, pagination, transactional merge, cancellation, retry limits, conflicts, and process restart recovery.
- Compose tests cover semantics, loading, empty, stale, partial failure, permission denial, confirmation, adaptive layout, and keyboard/touch access.
- Platform tests cover credential stores, filesystem paths and providers, background scheduling, external handoff, packaging, and lifecycle recovery.
Android folder capability cleanup uses demand-driven one-time WorkManager work. Empty stores and committed pairs do not keep cleanup work alive. Outstanding selections retain a retry owner until bound or abandoned; reconciliation preserves selections already delivered to an open setup. Folder-picker acquisition and durable scheduling run on the picker's owned IO scope, with result delivery on Main and cancellation cleanup retained on IO. Provider metadata queries run without account leases. Before taking a persisted grant, acquisition rechecks the exact active session and cancellation under both account identity leases. Account retirement invalidates outstanding chooser generations before cleanup, including failed cleanup; re-adding the same account does not revive an earlier chooser result, including equivalent server URL spellings. The canonical generation is retired even when cleanup no longer has credentials. A cancelled chooser retains its request slot until the platform returns its result, preventing delivery to a newer request. New acquisitions and cleanup requests schedule recovery, and unfinished cleanup retains bounded WorkManager backoff. A process restoration grace period protects pending folder drafts only while acquiring or ready selections remain after reconciliation; completed setup and cleanup return without waiting. A cancelled pair save preserves authoritative ownership recovery and then rethrows cancellation, even when the save committed. Abandoned acquisitions and committed pair removals retain cleanup evidence until access is released. Cleanup retries do not transfer or delete user file contents.
- Live-server audits use synthetic disposable accounts, record exact tested versions, and remain separate from deterministic unit and integration tests. A durably removed Android folder-sync pair reports completion while its previously scheduled capability recovery worker retries any remaining permission cleanup. Ambiguous coordinator saves still require authoritative confirmation of removal. Account retirement remains strict until its capability cleanup finishes, including current-process acquiring selections. Under the coordinator lock, authoritative pair snapshots reclaim ownerless current-process capability records after failed ownership transitions; configured pairs retain their grants. An unavailable restored grant abandons its exact saved reference without clearing a newer draft. Cancellation from grant, storage, and cipher adapters remains cancellation rather than being reported as damaged recovery metadata or deferred cleanup. Reconciliation records independent cleanup progress before reporting a failed provider, so one unavailable grant cannot indefinitely retain unrelated grants. A legacy shared root can regain expired access only for an account that still owns a recorded pair at that exact root in the authoritative coordinator.
A bug fix adds the smallest regression test at the layer where the invariant failed. Tests should assert public behavior, not copied implementation details.
Review checklist
Before merging an adapter or repository change, confirm:
- The responsibility is in the correct layer.
- Shared behavior is implemented once without hiding platform differences.
- Every write has provenance, permission, conflict, and confirmation policy.
- Cancellation survives all broad exception boundaries.
- Cache and queued-operation state survives interruption safely.
- Diagnostics identify the failed stage without private content.
- Tests cover success, empty, partial, offline, denied, malformed, cancelled, conflict, and retry-exhausted paths that apply.
Groupware compatibility failures
Contacts and Tasks fall back from multiget REPORT to individual object reads only for HTTP 405 or 501. Other server failures and throttling stop the affected collection refresh instead of multiplying requests across its objects. A failed collection remains a visible failure, not a successful empty result.
CardDAV and CalDAV multiget retain healthy records when another requested resource has an explicit 404 or 410 status, and reports the deletion count as a partial refresh. Every requested href must still be accounted for exactly once; omitted, duplicate, foreign, malformed, and other failed responses remain errors. A property-level 404 is not a resource deletion. These boundaries follow the CardDAV multiget response example and the CalDAV multiget contract, and the WebDAV resource and property status distinction.
Tasks track completed refreshes by calendar href, not display name or aggregate warning text. A missing selection is cleared after its own calendar completes, even if another calendar fails. Failed or budget-truncated calendars do not prove deletion; selection retains its calendar identity across restoration.
Whole-object task deletion requires one balanced VCALENDAR containing exactly one top-level VTODO and no sibling data components. Supporting VTIMEZONE and alarms owned by the task are allowed. VEVENT, VJOURNAL, other tasks, and unknown siblings withhold deletion, following the iCalendar component structure.
Dynamic collection header creates open the renderer's durable create form, never a standalone generic form. Header and inline actions share the same complete-baseline and postcondition recovery plan and require a pending mutation store. Missing recovery evidence withholds both actions. The header control is scoped to the active account and navigation context and cleared on disposal; pending writes remain in durable storage, not in that control.
Android publishes pending mutation records only after synchronizing the record and its containing directory. A publication failure withholds the request. External file handoff records cannot be restored or newly persisted while the durable handoff cleanup marker remains pending, including after restart.
Task requests, durable recovery storage, and recovery reads are serialized. Refresh and recovery-discard controls remain disabled until the active request finishes. A queued recovery read reloads the durable record after obtaining the operation lock, never verifying a captured pre-request state. Cancellation releases the operation lock without discarding the durable recovery record.
Task parsing withholds blank, control-containing, or over-1,024-character UIDs from editing. Recurrence identity values must have a valid date or date-time form, at most 16 characters, before entering task selection or saved state. Malformed exceptions are withheld rather than treated as masters; their raw components remain intact when another task is edited. Stable task keys include the DAV object, a length-delimited UID, and a master/exception discriminator. Duplicate component identities reject the affected calendar response rather than selecting one component or supplying duplicate list keys. Edits also require one unique matching component in the retained source. A failed calendar does not confirm that its selected task was deleted; other calendars remain available. Task-description normalization changes line endings only; leading, trailing, and whitespace-only content survives edits and recovery verification.
Loaded DAV object hrefs are opaque: updates and deletes preserve the discovered
href, including extensionless names, and require the loaded ETag. New objects
retain the generated .ics or .vcf suffix. The mutation builder still rejects
collection hrefs, unsafe paths, and mismatched content before a request.
Task details keep the full title, description, status, and errors in a bounded
scrolling body, with Edit and Delete outside it. Compact landscape and enlarged
text must not hide these actions or bypass read-only and recurrence guards.
Primary protocol references
- iCalendar recurrence identity
- Nextcloud WebDAV
- Nextcloud OCS API
- Nextcloud Activity API
- Nextcloud Client Integration API
- Notes API
Recovery of saved account access
Android and desktop startup distinguish an unreadable saved sign-in from a new installation. The native recovery screen offers retry and an explicit sign-in reset; background account lookup still returns unavailable without activating unreadable credentials. Unsupported credential versions remain protected from reset by older builds.
An explicit Android reset preserves malformed account-removal journal rows in private storage and writes a durable recovery fence before removing them from the active journal. Each account remains unavailable until its owned-state cleanup completes using that account's supplied session. Failed cleanup retains the fence and pending work, and new malformed rows invalidate earlier recovery decisions. This does not discard unresolved local document changes. Desktop account removal also verifies retirement of recognized temporary file-sync staging files under the sync engine lock, while keeping user originals. When a prepared removal is rolled back against retained credentials, desktop reopens both memory and persisted app-contract caches. Producers captured before retirement remain invalid after that recovery.
Android cleanup review markers are pruned against the retained account registry, then the account being recovered is recorded. Removing or replacing another account must not evict a retained account's marker and repeat cleanup of its offline pins or drafts. If registry ownership is unavailable, recovery keeps the existing markers until a readable registry can establish which accounts remain.
An unsupported desktop account registry produces a distinct startup state before credential recovery runs. It preserves the registry and secrets and directs the user to reopen a compatible app version, rather than opening a login flow whose save would be rejected by the same version fence. Malformed desktop registry data without a legacy recovery session produces a separate support-recovery state and preserves existing data. The established repair path from a valid legacy session remains available.
These are source and deterministic-test guarantees, not claims that a published installer already includes the behavior.
Android document writeback initialization holds the credential operation lease through metadata resolution and durable staging. Descriptor lifetime uses the removal fence, and close-time commits revalidate the exact session. Retained edits use canonical local account ownership; verified historical manifest owners migrate without changing staged bytes. Document change notifications include verified incarnation-scoped legacy IDs. Malformed optional aliases cannot block canonical reauthentication, and removal may discard them only after incarnation retirement.
Canonical writeback ownership is separate from the support diagnostic scope. Writeback failure events resolve the retained account's current diagnostic identity, so account support export and removal find the same scope as other provider errors.
Remote Android handoff producers capture a process generation under the current session guard before probing or staging. Account cleanup invalidates that generation even if durable clearing fails. Registration and managed-content publication check it under the registry lock, so late producers cannot republish cleared account metadata or content. Memory, streamed, Deck attachment, and historical version fallbacks guard private staging creation/promotion and the final external-app launch with the same generation; downloads run outside the registry lock. The durable cleanup marker separately fences restoration after a failed clear and process restart.
Retained-writeback diagnostics resolve the account's current session separately from the descriptor's captured session. Publication tries the current account operation lease and rechecks the exact session before enqueueing the diagnostic. Enqueueing captures the sink's account generation synchronously; the asynchronous sink rejects retired or stale generations under its persistence lock. Removal, further rotation or a busy lease skips this optional event rather than retaining a path-bearing event under an obsolete account scope.
Account removal also purges diagnostic scopes named by verified document aliases for that canonical account and incarnation. Both diagnostic sinks must finish before document grants and alias provenance are retired. A failure preserves those aliases for restart recovery; unrelated accounts and unknown hashes are never inferred to belong to the removed account. Deferred Android provider recovery uses the supplied session under the account removal lease before credentials are persisted. A sync pair retains its SAF grant until its pending local transactions and retirement have completed. Provider reconciliation runs outside the sync engine lock. Before retiring an account or removing a pair, the engine reacquires its lock and verifies that the selected pair snapshots have not changed; unrelated account state is preserved.
Legacy self-provider trees owned by a different account recover through their original provider URI and retained tree permission. Only a root matching the removing account may receive its supplied recovery session or a local authority rewrite. An unavailable original provider leaves recovery pending.
Android queued uploads retain their rows while saved credentials require recovery. Malformed preference values, damaged ciphertext, and invalid decoded credential records pause timed retries when no usable or temporarily inaccessible fallback remains. Temporary keystore failures continue retrying, including for inactive accounts. A malformed registry with a missing or permanently damaged aggregate also pauses retries; temporary aggregate access failures remain retryable. Unsupported credential versions require an upgrade. These policies are covered by deterministic Android unit tests; they do not establish device or release validation.
When the removal session is bound to the local provider's account, pending owned recovery tokens are discovered from that account's root, including directories moved outside the old sync subtree. The scan retains its depth, count, ownership, content-authentication, and cancellation bounds. Cross-account or external provider recovery stays within the original tree grant.
Expanded retirement discovery indexes only the selected tree's transactions and legacy transactions not proven to belong elsewhere. Seeing another tree's token in the same account or directory never authorizes its reconciliation.
Recovery authentication and retirement
Android self-provider recovery bypasses unversioned offline content and cached fallback reads. It requires a network listing before accepting generation-matched virtual content or opening an ETag-bound range source. Account-wide recovery-token discovery rejects multiple observed locations for an owned token without retiring its ownership record; unrelated tokens remain outside the selected recovery scope.
Recovery through another local account's legacy provider tree tries that account's lease without waiting and verifies its exact active session. The lease spans content authentication and reconciliation. It retains the original tree URI, grant and discovery scope while using authoritative provider reads. Busy, unavailable or unverified cross-profile accounts keep recovery pending. Ordinary external providers retain their existing grant behavior.
Relocated recovery can be attributed by either an authenticated stage or an authenticated backup. Backup-only delete transactions do not require a stage; both the recorded document identity and content identity are still required, and multiple observed locations remain ambiguous. Path-changing stage IDs require the original stage name and matching recorded content. Renamed backup IDs require matching recorded content even when the name contains the recovery token. Unverified token-bearing candidates preserve the ownership row without authorizing a rename or deletion.
Self-provider SAF recovery retains the exact ETag of a successfully completed content verification and uses it for the later delete or rename precondition. Failed or cancelled verification invalidates prior proof; a concurrent replacement cannot contribute its newer ETag to the mutation. Directory verification captures the authoritative collection generation before reading the complete tree. Restoration uses that original generation. Cleanup preserves a directory with a conditional, no-overwrite MOVE to a visible recovery name instead of recursively deleting descendants that may have changed meanwhile. Ordinary external-provider access retains its original platform contract.
Android account removal and folder recovery share one verified authority boundary. A supplied recovery session may address canonical document IDs or explicitly retained historical aliases only for the exact active or retired incarnation. The caller holds the account operation lease; recovery does not reload ordinary credentials or acquire a second blocking account lease. Cross-account recovery tries both canonical and legacy operation identities and remains pending when busy. Recovery listings preserve the validated parent identity so alias-scoped durable ownership can still find relocated children. Download retirement and the grouped diagnostic/grant cleanup execute in one failure boundary: unresolved downloads preserve grants and verified alias provenance for the next attempt.
A failed document cleanup retry resumes its validated incarnation-retirement token
in the same process. A mistyped preference retains a durable invalid prior state
for fail-closed rollback; storage I/O and cancellation errors still propagate.
Directory recovery folders use Recovered folder - <unique suffix> in
the original server parent. Failed or ambiguous moves retain a recovery path;
original and unrelated destination content is never overwritten.
Document/root resolver failures resolve a retained account's diagnostic session independently of the rejected document incarnation. Canonical document keys and verified aliases never become diagnostic storage keys themselves. Publication holds the current canonical/raw operation lease through the diagnostic sink's generation capture; missing, ambiguous, removed, or busy accounts publish nothing. Android folder-sync schedule restoration validates the exact session while holding its account operation lease, including server discovery. Permanent protocol or malformed-state failures stop the one-time job; temporary transport failures get at most two retries, including truncated network responses. A typed local-store truncation failure stops retries. Configured pairs remain intact for explicit recovery.
Credential-free legacy account cleanup uses the recorded document identity to match that account's legacy and full discovery-cache digests. A missing full digest is never a wildcard for other accounts' persisted contracts.
Desktop legacy credential migration propagates cancellation without returning an active session or classifying cancellation as a credential-store failure. The original legacy secret remains available for a later successful migration.
Cancelled external-handoff registration retains the persisted record across IO result delivery so it can revoke the capability and discard its staged copy before propagating cancellation. Android optional credential-slot repair also propagates cancellation before publishing its aggregate-backed session.
Before document retirement, malformed optional aliases do not block recovery through the verified current canonical account. Unknown historical aliases remain unauthorized, and alias-storage IO failures still leave recovery pending.
Large external-handoff staging validates its captured generation before file creation and promotion; network downloads remain outside the registry lock.
Ordinary reconciliation keeps live reselections abandonable; account retirement adopts matching configured-pair ownership before retiring drafts.