Table classes, the AppDatabase that stitches them together, focused query mixins for each concern, and the DriftXoloRepository that implements the abstract XoloRepository contract and translates raw Drift rows into domain entities. Nothing above this layer ever imports a Drift type directly — the presentation layer always talks to XoloRepository, never to AppDatabase.
Database Tables
AppDatabase registers eight Drift tables declared in tables.dart. Each table is a plain Dart class that extends Table; Drift’s code generator derives the companion and typed-row classes from it.
Collections
Collections
Represents both top-level workspace roots and nested folder nodes. The self-referential
parentId FK creates the tree structure; when parentId is null, the row is a root collection (exposed by the CollectionEntity.isRoot helper).SavedRequests
SavedRequests
Stores every reusable API request. A request can belong to a
Collections row (via collectionId) or be unclassified when that FK is null. Soft-deletion is modelled with isDeleted rather than a physical row delete, preserving history references.HistoryEntries
HistoryEntries
Automatic execution log. Every request fired from the UI appends a row here.
workspaceId links the entry to the active collection workspace; savedRequestId optionally links it back to the originating SavedRequests row.Environments
Environments
Named variable scopes (e.g. Dev, Staging, Production) that belong to a specific workspace collection. Setting
isActive to true marks the environment currently selected for variable resolution in that workspace.EnvVariables
EnvVariables
Stores key-value pairs scoped either to a specific environment (
scope = 'env', environmentId != null) or to a workspace globally (scope = 'global', collectionId != null, environmentId == null). When both FKs are null the variable is user-global.AppSettings
AppSettings
Simple key-value configuration store used for app-wide preferences (e.g. active workspace, last-run collection ID). The
key column is the primary key, so upserts are idiomatic.CollectionRuns
CollectionRuns
Persists the header record for every collection run execution. Step counts are updated atomically when
finishCollectionRun is called.RunStepResults
RunStepResults
One row per request step within a
CollectionRuns run. assertionResultsJson serialises the full List<AssertionResultEntity> so the UI can replay every pass/fail detail.DriftXoloRepository
DriftXoloRepository is the single concrete implementation of XoloRepository. It holds a private AppDatabase reference injected via its constructor and delegates every method to the appropriate query helper.
AppDatabase itself is split into focused part files — one per concern:
collection_queries.dart
CRUD, move, path traversal, and auth-data helpers for
Collections and SavedRequests.environment_queries.dart
Environment lifecycle and variable upsert/resolve logic.
history_queries.dart
watchRecentHistory, addHistoryItem, clear and single-entry helpers.run_queries.dart
createCollectionRun, finishCollectionRun, insertRunStepResultRow, and watchers.settings_queries.dart
setSetting, getSetting, watchSetting backed by AppSettings.DriftXoloRepository acts as a thin adapter: it calls the database method and pipes the result through a mapper function, never leaking Drift row types upstream.
Entity Mappers
entity_mappers.dart defines Dart extension methods on every Drift-generated row type. Each extension adds a toEntity() method that constructs the corresponding domain class, keeping all field mapping in one place.
mapCollections, mapSavedRequests, mapHistoryEntries, mapEnvironments, mapEnvVariables, mapCollectionRuns, mapRunStepResults) accept a List of Drift rows and return an immutable List of entities for use in .map(...) stream transformers inside DriftXoloRepository.
The presentation layer always works with domain entities and never imports anything from package:drift or package:xolo/data.
Reactive Streams
Drift exposes aStream<T> for every watch* query. These streams re-emit automatically whenever any row in the watched table changes — no manual invalidation required. DriftXoloRepository wraps each stream with .map(mapper) to convert rows into entities before they reach Riverpod.
Riverpod StreamProviders subscribe to these streams and expose them to the widget tree:
watch* method:
StreamProvider.autoDispose is used, the Drift query subscription is cancelled automatically when the last listener detaches, preventing background database activity when a screen is not visible.
Database Migrations
AppDatabase tracks schema evolution through an integer schemaVersion. When a new table or column is required, bump the version and add a guarded migration block inside onUpgrade.
The current version history shows how Xolo has grown:
1
v1 — Initial schema
Core tables:
SavedRequests, HistoryEntries, Collections, Environments, EnvVariables, AppSettings.2
v2 — Auth columns
Added
authType / authData to SavedRequests and HistoryEntries.3
v3 — Body schema
Added
schemaJson to SavedRequests for OpenAPI-driven body generation.4
v4 — Collection auth
Added
authType / authData to Collections for auth inheritance.5
v5 — Post-request scripts
Added
scriptsJson to SavedRequests for response-chaining rules.6
v6 — Pre-request scripts
Added
preScriptsJson to SavedRequests.7
v7 — Template URL history
Added
originalUrl to HistoryEntries to preserve the pre-substitution URL template.8
v8 — Collection runs
Added
assertionsJson to SavedRequests. Created CollectionRuns and RunStepResults tables.database.dart and add a guarded block:
Never access
AppDatabase directly from presentation code. All data access must go through xoloRepositoryProvider. This keeps the presentation layer decoupled from Drift, makes testing trivial (swap the provider with a mock), and ensures every query is mediated by the mapper layer.