Google ADK stores everything your agent knows — tool calls, user messages, conversation context — in plaintext SQLite. If that makes you uncomfortable, this post fixes it.
This is the recipe card. Ingredients, steps, done.
Prerequisites
- Python 3.12+
-
An existing ADK agent using
DatabaseSessionService(or a willingness to create a minimal one)
No system libraries, no C compilation, no Docker. The library is pure Python with two runtime dependencies: google-adk and cryptography. A short ingredient list.
Step 1: Install
pip install adk-secure-sessions
Or with uv:
uv add adk-secure-sessions
Verify the install:
python -c "import adk_secure_sessions; print('OK')"
Step 2: Swap the Import
Your agent code probably has something like this:
# Before — ADK default (unencrypted):
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(
db_url="sqlite+aiosqlite:///sessions.db"
)
Replace it with:
# After — encrypted:
from adk_secure_sessions import EncryptedSessionService, FernetBackend
session_service = EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=FernetBackend("your-secret-passphrase"),
)
Two changes: the import line and the constructor. Everything else in your agent stays the same — create_session, get_session, list_sessions, delete_session, append_event — the full ADK session lifecycle, identical behavior. The difference is what hits the disk.
Step 3: Use the Async Context Manager
For proper connection cleanup, wrap the service in async with. Here's a complete, runnable script:
import asyncio
from adk_secure_sessions import EncryptedSessionService, FernetBackend
async def main():
backend = FernetBackend("my-secret-passphrase")
async with EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=backend,
) as service:
# Create a session with sensitive state
session = await service.create_session(
app_name="my-agent",
user_id="user-123",
state={
"patient_name": "Jane Doe",
"diagnosis_code": "J06.9",
"api_key": "sk-secret-key-12345",
},
)
print(f"Created session: {session.id}")
# Retrieve — state is automatically decrypted
session = await service.get_session(
app_name="my-agent",
user_id="user-123",
session_id=session.id,
)
print(f"Decrypted state: {session.state}")
# List sessions for this app/user
response = await service.list_sessions(
app_name="my-agent",
user_id="user-123",
)
print(f"Sessions found: {len(response.sessions)}")
# Clean up when you're done
await service.delete_session(
app_name="my-agent",
user_id="user-123",
session_id=session.id,
)
print("Session deleted")
asyncio.run(main())
Copy this into a file and run it. The API behaves identically to ADK's DatabaseSessionService — same methods, same signatures, same return types. The only difference is what's stored on disk: switching from a glass jar to a lockbox. Same ingredients go in, same ingredients come out, but nobody can peek inside without the key.
Step 4: Verify the Encryption
Trust but verify. Open the SQLite database directly and confirm the data is actually encrypted.
Using the sqlite3 CLI:
sqlite3 sessions.db "SELECT state FROM sessions LIMIT 1;"
You'll see a base64-encoded string — the encrypted envelope — not readable JSON:
AQFnQUFBQUJuVm1Gc2RX...
Using Python:
import sqlite3
conn = sqlite3.connect("sessions.db")
row = conn.execute("SELECT state FROM sessions LIMIT 1").fetchone()
print(row[0][:60]) # First 60 chars of the encrypted envelope
conn.close()
What you won't see: {"patient_name": "Jane Doe", "diagnosis_code": "J06.9"}. That's the point. With DatabaseSessionService, anyone with file access reads your mise en place. With EncryptedSessionService, they see noise.
For a more convincing demo, run the basic usage example from the repo — it runs a real multi-turn ADK agent with Ollama and then inspects the raw database to prove no plaintext leaks. After a three-turn conversation about patient intake, the database contains zero occurrences of "Jane Doe" or "headache."
Step 5: Manage Your Passphrase
The passphrase is the only secret. Never hardcode it.
import os
from adk_secure_sessions import EncryptedSessionService, FernetBackend
backend = FernetBackend(os.environ["SESSION_KEY"])
Set it in your environment, your .env file, or your secrets manager. The library handles everything else — FernetBackend derives a cryptographic key using PBKDF2-HMAC-SHA256 with 480,000 iterations. You don't need to generate, store, or rotate raw key material.
If you use the wrong passphrase to read a session encrypted with a different one, you get a clear DecryptionError — never garbage data, never silent corruption.
What You Just Built
Five steps, plaintext to encrypted-at-rest:
-
Installed —
pip install adk-secure-sessions - Swapped — one import, one constructor change
- Ran — same API, encrypted storage
- Verified — the database contains ciphertext, not JSON
- Secured — passphrase in the environment, not the codebase
Your agent still works the same way. Your tests still pass. But the SQLite file is now useless without the key — like a walk-in freezer with a combination lock. Nothing changes about how the food is stored or retrieved, but the back door isn't open anymore.
Error Handling
When things go wrong, the library tells you what happened:
-
ConfigurationError— raised at startup if the backend is misconfigured. You'll catch this before any data is written. -
DecryptionError— raised if you read a session with the wrong key. The library never returns garbage.
from adk_secure_sessions import (
ConfigurationError,
DecryptionError,
EncryptedSessionService,
FernetBackend,
)
try:
async with EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=FernetBackend("correct-passphrase"),
) as service:
session = await service.get_session(
app_name="my-agent",
user_id="user-123",
session_id="some-session-id",
)
if session is None:
print("Session not found")
except ConfigurationError:
print("Backend doesn't conform to EncryptionBackend protocol")
except DecryptionError:
print("Wrong key — cannot decrypt session data")
Key Takeaways
-
One install, one import change —
pip install adk-secure-sessions, swap the constructor, done -
Full ADK lifecycle —
create_session,get_session,list_sessions,delete_session, andappend_eventall work identically - Verify it yourself — inspect the SQLite file to confirm ciphertext, not plaintext
- Passphrase management — use environment variables, never hardcode secrets
-
Clear errors —
DecryptionErrorfor wrong keys,ConfigurationErrorfor bad setup
Top comments (0)