Keep the engine's state in SQLite, not Postgres

One process owns this database — the image has run a single uvicorn
worker for that reason since the four-engines bug — so a file beside the
flows is the honest shape for it, and it is what lets `fluksio serve`
need no infrastructure at all. Live values, node execution and the work
queue never came here anyway; what does is a rollup a minute at a time,
a row per cascade and the run history, and WAL keeps the readers going
while that one writer works.

DATA_DIR is now the one setting that moves everything an installation
keeps; the rest derive from it and the images still spell theirs out.
The schema is prepared in-process at startup, so the prestart service is
gone, and the ten Postgres-only revisions collapse into one portable
baseline.

Three things only worked because psycopg was casting for us: a token's
subject arriving as a string where the column is a UUID, `greatest`, and
`date_bin`. The timestamps needed a column type of their own — SQLite
stores no offset, and a naive datetime read back either raises against an
aware `now` or serialises as local time.

Postgres stays in the stack only for Umami, behind the analytics profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 22:19:45 +02:00
co-authored by Claude Opus 5
parent 2c369ac75f
commit 961a8f881d
51 changed files with 841 additions and 1089 deletions
+26 -18
View File
@@ -3,9 +3,11 @@ from datetime import datetime, timezone
from typing import Any
from pydantic import EmailStr
from sqlalchemy import JSON, Column, DateTime
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
from fluksio.core.types import UTCDateTime
def get_datetime_utc() -> datetime:
return datetime.now(timezone.utc)
@@ -57,7 +59,7 @@ class User(UserBase, table=True):
portal_sub: str | None = Field(default=None, max_length=64, unique=True, index=True)
created_at: datetime | None = Field(
default_factory=get_datetime_utc,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
@@ -118,7 +120,9 @@ class OAuthClient(SQLModel, table=True):
client_name: str = Field(max_length=128)
redirect_uris: list[str] = Field(sa_column=Column(JSON), default_factory=list)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), nullable=False
default_factory=lambda: datetime.now(timezone.utc),
nullable=False,
sa_type=UTCDateTime,
)
@@ -138,12 +142,14 @@ class OAuthAuthorizationCode(SQLModel, table=True):
redirect_uri: str = Field(max_length=2048)
code_challenge: str = Field(max_length=128)
resource: str | None = Field(default=None, max_length=2048)
expires_at: datetime
used_at: datetime | None = None
expires_at: datetime = Field(sa_type=UTCDateTime)
used_at: datetime | None = Field(default=None, sa_type=UTCDateTime)
#: The refresh token this code produced, so replaying the code can revoke it.
refresh_token_id: uuid.UUID | None = None
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), nullable=False
default_factory=lambda: datetime.now(timezone.utc),
nullable=False,
sa_type=UTCDateTime,
)
@@ -163,10 +169,12 @@ class OAuthRefreshToken(SQLModel, table=True):
#: Every token rotated out of one authorization shares this, so reusing an
#: old one can revoke the whole line rather than just itself.
family_id: uuid.UUID
expires_at: datetime
expires_at: datetime = Field(sa_type=UTCDateTime)
revoked: bool = False
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), nullable=False
default_factory=lambda: datetime.now(timezone.utc),
nullable=False,
sa_type=UTCDateTime,
)
@@ -231,7 +239,7 @@ class MetricBucket(SQLModel, table=True):
bucket: datetime = Field(
primary_key=True,
index=True,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
executions: int = 0
errors: int = 0
@@ -255,7 +263,7 @@ class EngineEvent(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
ts: datetime = Field(
index=True,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
#: node_error, flow_quarantined, engine_degraded, …, or audit.
type: str = Field(max_length=32, index=True)
@@ -279,11 +287,11 @@ class FlowRun(SQLModel, table=True):
source: str = ""
started_at: datetime = Field(
index=True,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
finished_at: datetime | None = Field(
default=None,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
#: running, ok, error or abandoned.
status: str = "running"
@@ -337,15 +345,15 @@ class Run(SQLModel, table=True):
labels: list[str] = Field(sa_column=Column(JSON), default_factory=list)
created_at: datetime = Field(
index=True,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
started_at: datetime | None = Field(
default=None,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
finished_at: datetime | None = Field(
default=None,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
duration_ms: float = 0.0
#: The flow's declared outputs once it finished.
@@ -355,7 +363,7 @@ class Run(SQLModel, table=True):
engine: str = Field(default="", max_length=64)
lease_at: datetime | None = Field(
default=None,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
actor: str = Field(default="", max_length=255)
@@ -372,7 +380,7 @@ class RunNode(SQLModel, table=True):
attempt: int = 1
started_at: datetime | None = Field(
default=None,
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)
duration_ms: float = 0.0
#: Which worker ran it: "local", or a remote worker's name.
@@ -417,5 +425,5 @@ class RunArtifact(SQLModel, table=True):
media_type: str = Field(default="application/octet-stream", max_length=128)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_type=DateTime(timezone=True), # type: ignore
sa_type=UTCDateTime,
)