Freshness: 2026-03-30
Cross-refs: INDEX.md | backend.md | workers.md
FlowPilot uses SQLite / Turso (via tursodatabase/libsql-go) for all persistence. The same driver supports both a local embedded file and a remote Turso distributed database. The internal/database package contains:
config.go—DatabaseConfig,DatabaseType, andDetectType()URL classifiersqlite.go— connection management, schema creation, migrations, Turso/local branchingdb_*.gofiles — one DAO file per domain
The internal/models package defines all shared data structs (no ORM; raw database/sql).
// DatabaseType selects the backend driver.
type DatabaseType int
const (
DatabaseSQLite DatabaseType = iota // local file or :memory:
DatabaseTurso // libsql:// remote URL
)
// DatabaseConfig is the single config value passed to NewWithConfig.
type DatabaseConfig struct {
URL string // local file path OR libsql:// URL
AuthToken string // Turso auth token (optional)
LocalPath string // local replica path for embedded replica mode (optional)
}
// DetectType inspects the DSN prefix to pick the driver.
func DetectType(dsn string) DatabaseTypeDetectType returns DatabaseTurso when the URL starts with libsql://; otherwise DatabaseSQLite.
type DB struct {
conn *sql.DB // writer
readConn *sql.DB // reader (== conn for Turso)
}
// Backward-compatible: opens a local SQLite file.
func New(dbPath string) (*DB, error)
// Config-based: supports local SQLite and Turso.
func NewWithConfig(config DatabaseConfig) (*DB, error)- Single shared pool (max 1 open) for both reads and writes via
libsqldriver - Using two separate
*sql.DBhandles on the same file causesdatabase is locked; a shared pool avoids this - PRAGMAs applied:
synchronous=NORMAL,cache_size=-64000,mmap_size=268435456,temp_store=MEMORY
- Single shared
*sql.DBfor reads and writes (remote is stateless) - Auth token appended as
?authToken=query parameter - PRAGMAs not applied (unsupported over remote connection)
- Embedded replica mode:
libsql.NewEmbeddedReplicaConnector(localPath, remoteURL)— offline-capable local SQLite that syncs periodically to Turso
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
(none) | libsql:// URL — activates Turso mode |
TURSO_AUTH_TOKEN |
(none) | Auth token passed to Turso |
DATABASE_PATH |
<dataDir>/tasks.db |
Local file; doubles as embedded replica path when DATABASE_URL is set |
CREATE TABLE tasks (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL,
url TEXT NOT NULL,
status TEXT NOT NULL, -- pending|queued|running|done|failed|cancelled
priority INTEGER DEFAULT 0,
tags TEXT, -- JSON array string
steps TEXT, -- JSON array of TaskStep
proxy_config TEXT, -- JSON ProxyConfig
logging_policy TEXT, -- JSON TaskLoggingPolicy
flow_id TEXT, -- FK → recorded_flows.id (nullable)
batch_id TEXT, -- FK → batch_groups.id (nullable)
schedule_id TEXT, -- FK → schedules.id (nullable)
result TEXT, -- JSON TaskResult (nullable)
error TEXT,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
started_at DATETIME,
completed_at DATETIME
)CREATE TABLE recorded_flows (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
steps TEXT NOT NULL, -- JSON []RecordedStep
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE dom_snapshots (
id TEXT PRIMARY KEY,
flow_id TEXT NOT NULL REFERENCES recorded_flows(id),
step_index INTEGER NOT NULL,
html TEXT NOT NULL,
screenshot BLOB, -- PNG bytes
created_at DATETIME
)CREATE TABLE batch_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
flow_id TEXT, -- FK → recorded_flows.id (nullable)
total_tasks INTEGER DEFAULT 0,
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE schedules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
cron_expr TEXT NOT NULL,
task_input TEXT NOT NULL, -- JSON TaskInput template
enabled INTEGER DEFAULT 1,
last_run_at DATETIME,
next_run_at DATETIME,
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE proxies (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL,
protocol TEXT NOT NULL, -- http|https|socks4|socks5
username TEXT, -- encrypted (AES-256-GCM)
password TEXT, -- encrypted (AES-256-GCM)
status TEXT DEFAULT 'active',
last_used_at DATETIME,
last_checked_at DATETIME,
fail_count INTEGER DEFAULT 0,
tags TEXT, -- JSON array
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE proxy_routing_presets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
rules TEXT NOT NULL, -- JSON []RoutingRule
strategy TEXT NOT NULL, -- round_robin|random|least_used|sticky
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE captcha_configs (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL, -- anticaptcha|2captcha
api_key TEXT NOT NULL, -- encrypted
enabled INTEGER DEFAULT 1,
created_at DATETIME,
updated_at DATETIME
)CREATE TABLE task_events (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
event_type TEXT NOT NULL, -- created|started|completed|failed|cancelled|retried
metadata TEXT, -- JSON additional info
created_at DATETIME
)CREATE TABLE step_logs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
step_index INTEGER NOT NULL,
action TEXT NOT NULL,
selector TEXT,
value TEXT,
status TEXT NOT NULL, -- ok|error|skipped
error TEXT,
duration_ms INTEGER,
created_at DATETIME
)CREATE TABLE network_logs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
request_id TEXT,
url TEXT NOT NULL,
method TEXT NOT NULL,
status_code INTEGER,
request_headers TEXT, -- JSON
response_headers TEXT, -- JSON
request_body TEXT,
response_body TEXT,
duration_ms INTEGER,
created_at DATETIME
)CREATE TABLE websocket_logs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
request_id TEXT,
url TEXT NOT NULL,
event_type TEXT NOT NULL, -- opened|closed|message_sent|message_received
payload TEXT,
created_at DATETIME
)CREATE TABLE visual_baselines (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
step_index INTEGER NOT NULL,
image BLOB NOT NULL, -- PNG bytes
created_at DATETIME,
updated_at DATETIME,
UNIQUE(task_id, step_index)
)CREATE TABLE visual_diffs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
step_index INTEGER NOT NULL,
baseline_id TEXT NOT NULL REFERENCES visual_baselines(id),
diff_image BLOB, -- PNG diff overlay
diff_percent REAL NOT NULL, -- 0.0–100.0
passed INTEGER NOT NULL, -- 1=pass, 0=fail
threshold REAL NOT NULL,
created_at DATETIME
)Schema statements are executed one-by-one (not as a single multi-statement batch) for compatibility with the libsql remote driver.
The migrate() function runs once at startup, using CREATE TABLE IF NOT EXISTS and ALTER TABLE ... ADD COLUMN IF NOT EXISTS patterns. It is idempotent — safe to run on existing databases.
Migration strategy: additive only (no destructive changes). Version is not tracked explicitly; the IF NOT EXISTS guards ensure safety.
| Function | Description |
|---|---|
CreateTask(task) |
INSERT into tasks |
GetTask(id) |
SELECT by PK |
ListTasks(filter) |
SELECT with WHERE clause built from filter |
ListTasksPaginated(filter, page, size) |
+ LIMIT/OFFSET |
UpdateTask(id, fields) |
UPDATE specific columns |
UpdateTaskStatus(id, status) |
UPDATE status + timestamps |
DeleteTask(id) |
DELETE by PK |
SetTaskResult(id, result) |
UPDATE result JSON |
IncrementRetryCount(id) |
UPDATE retry_count++ |
| Function | Description |
|---|---|
SaveFlow(flow) |
INSERT OR REPLACE |
GetFlow(id) |
SELECT + parse steps JSON |
ListFlows() |
SELECT metadata (no steps) |
DeleteFlow(id) |
DELETE + cascade dom_snapshots |
SaveDOMSnapshot(snap) |
INSERT into dom_snapshots |
GetDOMSnapshot(flowID, stepIndex) |
SELECT by composite key |
| Function | Description |
|---|---|
CreateBatchGroup(group) |
INSERT batch_groups |
GetBatchGroup(id) |
SELECT by PK |
ListBatchGroups() |
SELECT all |
GetBatchProgress(batchID) |
COUNT tasks by status |
UpdateBatchTaskCount(id, n) |
UPDATE total_tasks |
| Function | Description |
|---|---|
InsertTaskEvent(event) |
INSERT into task_events |
ListAuditTrail(filter) |
SELECT task_events with filter |
InsertStepLog(log) |
INSERT into step_logs |
ListStepLogs(taskID) |
SELECT step_logs for task |
InsertNetworkLog(log) |
INSERT into network_logs |
ListNetworkLogs(taskID) |
SELECT network_logs for task |
InsertWebSocketLog(log) |
INSERT into websocket_logs |
ListWebSocketLogs(taskID) |
SELECT websocket_logs for task |
| Function | Description |
|---|---|
AddProxy(proxy) |
INSERT with encrypted credentials |
ListProxies() |
SELECT all (credentials decrypted in manager) |
GetProxy(id) |
SELECT by PK |
UpdateProxy(id, fields) |
UPDATE |
DeleteProxy(id) |
DELETE |
MarkProxyUsed(id) |
UPDATE last_used_at, fail_count reset |
MarkProxyFailed(id) |
UPDATE fail_count++ |
ListActiveSProxies() |
SELECT WHERE status='active' |
| Function | Description |
|---|---|
PurgeOldData(days) |
DELETE tasks/logs/events older than N days |
Cascade: deletes tasks → cascades to step_logs, network_logs, websocket_logs, task_events, visual_diffs (via FK ON DELETE CASCADE).
| Function | Description |
|---|---|
SaveBaseline(baseline) |
INSERT OR REPLACE into visual_baselines |
GetBaseline(taskID, stepIndex) |
SELECT by composite key |
SaveVisualDiff(diff) |
INSERT into visual_diffs |
ListVisualDiffs(taskID) |
SELECT for task |
type Task struct {
ID string
Name string
URL string
Status TaskStatus // "pending"|"queued"|"running"|"done"|"failed"|"cancelled"
Priority int
Tags []string
Steps []TaskStep
ProxyConfig *ProxyConfig
LoggingPolicy TaskLoggingPolicy
FlowID string
BatchID string
ScheduleID string
Result *TaskResult
Error string
RetryCount int
MaxRetries int
CreatedAt time.Time
// ... timestamps
}
type TaskStep struct {
Action string // models.ActionClick etc.
Selector string
Value string
Condition *ConditionConfig
Options map[string]any
}
// Action constants (50+):
const (
ActionClick = "click"
ActionType = "type"
ActionNavigate = "navigate"
ActionScreenshot = "screenshot"
ActionSolveCaptcha = "solve_captcha"
// ...
)type RecordedFlow struct {
ID string
Name string
Description string
Steps []RecordedStep
CreatedAt time.Time
UpdatedAt time.Time
}
type RecordedStep struct {
Index int
Action string
Selector string
Value string
URL string
Timestamp int64
Selectors []SelectorCandidate
}type Proxy struct {
ID string
Label string
Host string
Port int
Protocol ProxyProtocol // "http"|"https"|"socks4"|"socks5"
Username string // decrypted in memory
Password string // decrypted in memory
Status ProxyStatus // "active"|"inactive"|"failed"
// ...
}
type ProxyRoutingPreset struct {
ID string
Name string
Rules []RoutingRule
Strategy RotationStrategy // "round_robin"|"random"|"least_used"|"sticky"
}func ClassifyError(err error) ErrorCode {
// maps error strings to:
// ErrorCodeTimeout, ErrorCodeNetworkError, ErrorCodeElementNotFound,
// ErrorCodeCaptchaFailed, ErrorCodeProxyFailed, ErrorCodeUnknown
}- turso-integration.md — Full Turso setup, env vars, and deployment guide
- backend.md — App methods that call these DAOs
- integrations.md — credential encryption via crypto package
- workers.md — queue reads/writes task status