Problem
Multiple foundational documents describe adk-secure-sessions as having an "independent schema" and being "just an encryption layer." Investigation revealed both claims are inaccurate, and the current architecture has a ceiling that blocks multi-database support.
What we discovered
1. Our schema mirrors ADK, it's not independent
Our 4 tables (app_states, user_states, sessions, events) are a copy of ADK's SqliteSessionService schema with TEXT → BLOB for encrypted columns and an added version column. Same table names, same columns, same structure.
2. We're a full session service replacement, not an encryption layer
We reimplement all persistence logic (CRUD, state splitting, event storage, connection management) in addition to encryption. Every customer-facing doc says "encryption layer" or "drop-in replacement for DatabaseSessionService" — neither is accurate. We implement BaseSessionService from scratch with our own persistence.
3. ADR-000's decorator rejection reasoning is stale
ADR-000 rejected wrapping because append_event used json_patch SQL operations with no interception point. That was true for V0's per-column event schema. ADK V1 changed this — events are now a single event_data JSON blob, and DatabaseSessionService merges state in Python (dict | delta), not SQL.
4. Architecture B (current) was right for MVP, but has a ceiling
For SQLite-only MVP, reimplementing persistence in raw aiosqlite was pragmatic. But to mature beyond SQLite, we'd have to reimplement everything ADK already provides:
- PostgreSQL/MySQL/MariaDB dialect handling
- Connection pooling with
pool_pre_ping
- Row-level locking (
SELECT ... FOR UPDATE)
- Schema version detection and migration
GetSessionConfig filtering
- Timestamp dialect normalization
5. SqliteSessionService cannot be wrapped
SqliteSessionService uses json_patch(state, excluded.state) — a SQLite JSON function that merges deltas in SQL. The state column MUST be valid JSON text. Encrypted data (BLOB or base64) breaks this. This is the same blocker ADR-000 identified, just in a different service.
6. DatabaseSessionService CAN be wrapped
DatabaseSessionService V1 loads state into Python, merges with dict | delta, writes back through SQLAlchemy. No SQL-level JSON operations. A SQLAlchemy TypeDecorator can intercept at the ORM boundary — encrypt on write, decrypt on read. SQLAlchemy is already in our dep tree via google-adk.
7. Table name collision risk
ADK's tables and our tables share identical names. Pointing both at the same database would collide. Wrapping DatabaseSessionService eliminates this — we'd use their tables with encrypted column types.
Architecture decision needed
To mature beyond SQLite-only, wrap DatabaseSessionService via SQLAlchemy TypeDecorator. This gives us:
- SQLite + PostgreSQL + MySQL + MariaDB encryption — for free
- Row-level locking, connection pooling, dialect handling — for free
- Schema migrations when ADK evolves — for free
- Focus solely on encryption — our actual value-add
- No table name collision (we use their tables)
The encryption backends (Fernet, AES-256-GCM) are properly abstracted behind EncryptionBackend protocol and survive this change untouched.
Blast radius
10 HIGH / 8 MEDIUM / 8 LOW severity findings across docs, source, and planning artifacts.
HIGH — must fix
| File |
Issue |
pyproject.toml:4 |
"drop-in replacement for DatabaseSessionService" |
README.md:13 |
Same |
CLAUDE.md:11 |
"own schema independent of ADK" |
.claude/rules/conventions.md:35-39 |
"Own Our Schema" — lists 2 of 4 tables, "independent" claim |
docs/adr/ADR-004 |
Entire "own schema" narrative |
docs/adr/ADR-000:45 |
Stale V0-era rejection reasoning |
docs/ROADMAP.md:5 |
"missing encryption layer" |
docs/index.md:13 |
"drop-in replacement for DatabaseSessionService and SqliteSessionService" |
src/__init__.py:10-11 |
"Drop-in replacement for DatabaseSessionService" |
src/services/encrypted_session.py:3,123 |
Same in module + class docstrings |
MEDIUM — should fix
ADR-000 additional lines, ARCHITECTURE.md, index.md feature bullet, project-overview.md, docstring-templates.md, services/init.py, PRD line 334, planning architecture
LOW — minor / historical
CHANGELOG entries, epics, project-context.md, implementation artifacts
Action items
Phase A: Documentation honesty (can do now)
Phase B: Architecture migration (before Phase 3 epics)
Phase C: Sub-issues
Problem
Multiple foundational documents describe adk-secure-sessions as having an "independent schema" and being "just an encryption layer." Investigation revealed both claims are inaccurate, and the current architecture has a ceiling that blocks multi-database support.
What we discovered
1. Our schema mirrors ADK, it's not independent
Our 4 tables (
app_states,user_states,sessions,events) are a copy of ADK'sSqliteSessionServiceschema withTEXT→BLOBfor encrypted columns and an addedversioncolumn. Same table names, same columns, same structure.2. We're a full session service replacement, not an encryption layer
We reimplement all persistence logic (CRUD, state splitting, event storage, connection management) in addition to encryption. Every customer-facing doc says "encryption layer" or "drop-in replacement for DatabaseSessionService" — neither is accurate. We implement
BaseSessionServicefrom scratch with our own persistence.3. ADR-000's decorator rejection reasoning is stale
ADR-000 rejected wrapping because
append_eventusedjson_patchSQL operations with no interception point. That was true for V0's per-column event schema. ADK V1 changed this — events are now a singleevent_dataJSON blob, andDatabaseSessionServicemerges state in Python (dict | delta), not SQL.4. Architecture B (current) was right for MVP, but has a ceiling
For SQLite-only MVP, reimplementing persistence in raw aiosqlite was pragmatic. But to mature beyond SQLite, we'd have to reimplement everything ADK already provides:
pool_pre_pingSELECT ... FOR UPDATE)GetSessionConfigfiltering5. SqliteSessionService cannot be wrapped
SqliteSessionServiceusesjson_patch(state, excluded.state)— a SQLite JSON function that merges deltas in SQL. Thestatecolumn MUST be valid JSON text. Encrypted data (BLOB or base64) breaks this. This is the same blocker ADR-000 identified, just in a different service.6. DatabaseSessionService CAN be wrapped
DatabaseSessionServiceV1 loads state into Python, merges withdict | delta, writes back through SQLAlchemy. No SQL-level JSON operations. A SQLAlchemyTypeDecoratorcan intercept at the ORM boundary — encrypt on write, decrypt on read. SQLAlchemy is already in our dep tree viagoogle-adk.7. Table name collision risk
ADK's tables and our tables share identical names. Pointing both at the same database would collide. Wrapping
DatabaseSessionServiceeliminates this — we'd use their tables with encrypted column types.Architecture decision needed
To mature beyond SQLite-only, wrap
DatabaseSessionServicevia SQLAlchemyTypeDecorator. This gives us:The encryption backends (Fernet, AES-256-GCM) are properly abstracted behind
EncryptionBackendprotocol and survive this change untouched.Blast radius
10 HIGH / 8 MEDIUM / 8 LOW severity findings across docs, source, and planning artifacts.
HIGH — must fix
pyproject.toml:4README.md:13CLAUDE.md:11.claude/rules/conventions.md:35-39docs/adr/ADR-004docs/adr/ADR-000:45docs/ROADMAP.md:5docs/index.md:13src/__init__.py:10-11src/services/encrypted_session.py:3,123MEDIUM — should fix
ADR-000 additional lines, ARCHITECTURE.md, index.md feature bullet, project-overview.md, docstring-templates.md, services/init.py, PRD line 334, planning architecture
LOW — minor / historical
CHANGELOG entries, epics, project-context.md, implementation artifacts
Action items
Phase A: Documentation honesty (can do now)
Phase B: Architecture migration (before Phase 3 epics)
DatabaseSessionServicewith SQLAlchemyTypeDecoratorencrypted_session.pyfrom ~800 lines raw SQL to thin wrapperPhase C: Sub-issues