Feature: Storage Layer #3
Labels
No labels
area:api
area:core
area:docs
area:infra
area:ux
dependencies
documentation
duplicate
good first issue
help wanted
invalid
question
rust
status:complete
status:partial
status:planned
type:bug
type:design
type:feature
type:infra
type:refactor
type:research
type:ux
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
icub3d/decentcom#3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Migrated from GitHub issue icub3d/decentcom#3
Original Author: @icub3d
Original Date: 2026-04-15T14:15:20Z
Feature: Storage Layer
Overview
Define the storage trait hierarchy that abstracts all persistence operations, then implement the SQLite backend as the default. This gives the server a working database for users, messages, channels, and sessions while keeping the door open for PostgreSQL and other backends later.
Background
The storage design doc (storage.md) specifies a pluggable backend architecture with traits for
UserStore,MessageStore,ChannelStore,MediaStore, andSessionStore. SQLite + local disk is the default backend requiring zero external dependencies. The architecture doc (architecture.md) notes that session tokens may be kept in-memory rather than in the primary database. The server-config feature provides theStorageConfigstruct that selects which backend to initialize.Requirements
sharedcrate (or a dedicatedstoragemodule inserver) with async methodsUserStore,MessageStore,ChannelStore,SessionStoreMediaStoretrait is defined but the implementation is deferred to the file-uploads featureResult<T, StorageError>with a unified error typeDesign
API / Interface Changes
No new HTTP endpoints. This feature provides internal infrastructure used by other features.
Data Model Changes
SQLite schema (managed via sqlx migrations):
Component Changes
New files:
Modified files:
Key trait definitions (in
server/src/storage/traits.rs):Task List
sqlx(withsqliteandruntime-tokiofeatures),async-trait, andulid(oruuid) toserver/Cargo.tomlserver/src/storage/mod.rswithStorageErrorenum and a compositeStoragetrait (or struct wrapping all sub-traits)server/src/storage/models.rs:User,Message,Channel,Category,SessionUserStore,MessageStore,ChannelStore,SessionStore, andMediaStoretraits inserver/src/storage/traits.rsserver/migrations/001_initial.sqlwith the schemaSqliteStoragestruct inserver/src/storage/sqlite/mod.rswith pool initialization and migration runningUserStoreforSqliteStorageinserver/src/storage/sqlite/users.rsChannelStoreforSqliteStorageinserver/src/storage/sqlite/channels.rsMessageStoreforSqliteStorageinserver/src/storage/sqlite/messages.rswith cursor-based paginationSessionStoreforSqliteStorageinserver/src/storage/sqlite/sessions.rsserver/src/main.rs: read config, createSqliteStorage, inject into axum statedelete_expired_sessionsperiodically)Test List
SqliteStorageinitializes with an in-memory database and runs migrations successfullyUserStore— create user, get by id, get by pubkey, update, listUserStore— duplicate pubkey returns appropriate errorChannelStore— create, get, list, update, deleteMessageStore— create message, get by id, list by channel with orderingMessageStore— cursor-based pagination returns correct pagesMessageStore— soft delete setsdeletedflag, message no longer appears in listSessionStore— create session, retrieve by token, verify expiry timeSessionStore—delete_expired_sessionsremoves only expired sessionsStorageErrorvariants cover not-found, conflict, and internal error cases/healthstill respondsImplementation Notes
sqlx::query()with.bind()andRow::get()rather than thequery!()compile-time macros. The macros infer column nullability inconsistently for SQLite depending on whether a WHERE clause is present, causing type mismatches betweenTandOption<T>for the same NOT-NULL column. Runtime queries sidestep this entirely.SqliteStorageholds aMutex<ulid::Generator>so IDs are monotonically increasing even when multiple records are created within the same millisecond. This ensures consistent ordering in message pagination tests and production.max_connections(1)because each SQLite:memory:connection is a separate isolated database; without this, migrations run on one connection but queries land on another.server/src/storage/models.rs; themediatable is included in001_initial.sql.Open Questions
shared(so the client Tauri core can also use them) or inserver/src/storage/models.rs? If they live inshared, the client gets the sameUser,Message, etc. structs for deserialization. This is likely the right call but meanssharedgains a dependency onserde(which it probably already has).MediaStoretrait is defined here but not implemented. Should it include just the trait definition, or also themediatable migration? Including the migration keeps all schema in one place; deferring it avoids unused tables.