Appcelerator Titanium Alloy Models with Ti.Database SQLite Persistence
Architecture notes for persisting Alloy models to SQLite in Titanium, including schema boundaries, encryption, migrations, and UI-thread checks.
05 Aug 2026, 08:02 UTC

Problem and takeaway
You are building a cross-platform mobile app with Appcelerator Titanium and Alloy, and you need structured data that survives restarts and works offline. Alloy models and collections are convenient for UI state, but they are not a durability layer by themselves. Ti.Database provides synchronous access to a per-app SQLite file, yet it leaves schema, encryption, and migration decisions to you.
Use this design when you need a small, self-contained local database on iOS and Android: keep Alloy models for presentation and validation, route all durable writes through a single Ti.Database module, encrypt sensitive fields before they reach SQLite, and manage schema changes with manual migrations. Do not use this pattern if you need encrypted storage by default, multi-process access, or a managed sync backend.
Requirements
- Offline-first structured storage with the same schema on iOS and Android.
- Existing rows must survive an app upgrade.
- Sensitive fields must remain unreadable to native modules and third-party libraries that can access the application filesystem.
- CI builds must be reproducible against pinned Titanium SDK and platform toolchain versions.
Smallest suitable design
Create one persistence module, conventionally lib/db.js, that owns the SQLite file. Alloy controllers ask this module for data; they never call Ti.Database directly. The module opens the database, ensures base tables exist, and applies migrations from a stored version number.
// lib/db.js (conceptual; adapt to your Titanium SDK version)
var db = Ti.Database.open('app_local');
db.execute('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER)');
db.execute('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, payload TEXT, updated_at INTEGER)');
var row = db.execute('SELECT version FROM schema_version LIMIT 1');
var current = row.isValidRow() ? row.field(0) : 0;
row.close();
if (current < 2) {
db.execute('ALTER TABLE items ADD COLUMN synced INTEGER DEFAULT 0');
db.execute(current === 0 ? 'INSERT INTO schema_version (version) VALUES (2)' : 'UPDATE schema_version SET version = 2');
}
db.close();
Alloy models then read and write through helper functions in the same module. This keeps the SQL dialect in one place and makes it easier to replace SQLite later without touching controllers.
Trust and data boundaries
Ti.Database stores the SQLite file in the application sandbox directory. That boundary is weaker than it sounds: any native module or third-party library with filesystem access can read the file, and Titanium does not encrypt SQLite contents at rest. Mobile operating systems may also back up the application directory to cloud services unless you explicitly exclude it.
Treat fields such as authentication tokens, personal identifiers, and health data as sensitive. Encrypt them in the application layer before inserting, and decrypt after reading. Store encryption keys in the platform secure store where available, not in the SQLite file or in source code.
Operational checks
Run these checks on a clean build before relying on local persistence.
1. Confirm the build toolchain
Where to run: project root, in the terminal. Required permissions: none beyond normal CLI access.
appc ti info
Expected check: the command reports the installed Titanium SDK version and available platform SDKs. Risk: an outdated CLI or missing Xcode or Android SDK will fail the build, so pin versions in CI and do not upgrade the Titanium SDK without regression testing native modules.
2. Inspect the SQLite file on a device or emulator
Where to run: host machine, after copying the app's application data directory from the device or emulator. Required permissions: read access to the extracted sandbox directory.
sqlite3 /path/to/appdata/app_local.sqlite ".schema"
sqlite3 /path/to/appdata/app_local.sqlite "SELECT count(*) FROM items;"
Expected check: tables and columns match the migration version, and row counts match application logic. Risk: the file may be locked while the app is running; stop the app or copy the file before querying.
3. Measure UI-thread impact
Where to run: debug build on a physical device or emulator. Required permissions: none.
Expected check: insert 1,000 rows in a single transaction and observe whether the main thread stalls. Repeat with individual INSERT statements outside a transaction; the difference is usually large enough to justify batching. Risk: do not run bulk imports during screen transitions or user interactions.
Failure modes
- UI blocking — Ti.Database executes synchronously on the JavaScript thread. Large queries or unbatched inserts can freeze the interface.
- Partial migration — if a migration fails halfway, the database may be left in an inconsistent state. Keep a pre-migration copy of the SQLite file and restore it if the upgrade fails; this is the rollback path for a state-changing migration.
- Native module breakage — native modules compiled against one Titanium SDK major version often fail against another. Lock SDK and dependency versions in CI.
- Backup exposure — the SQLite file may be included in iCloud or Android backups, exposing data outside the app sandbox unless backup exclusion is configured.
Conditions that would change the design
- If you must store regulated or financial data, add application-layer encryption or evaluate an encrypted SQLite distribution; do not rely on the sandbox alone.
- If you need to import more than a few thousand rows at startup, perform the import on a background screen in small batches rather than blocking the first render.
- If you need synchronization with a remote API, add an adapter layer between Alloy collections and the network, with conflict resolution for offline writes.
- If Axway support status for your Titanium SDK version is uncertain, verify current support terms before starting new development or consider pinning to a supported release.
Practical verification
After a build, open the copied SQLite file with sqlite3, confirm schema_version holds the expected number, and run a read path through an Alloy controller to verify the returned rows match the file contents. If any check fails, do not ship the migration until the schema and access path are consistent on both iOS and Android builds.
Note: this note summarizes well-established Titanium behavior. The research context flags that current vendor support status and SDK version compatibility require verification before adoption; treat version-specific API details as subject to your installed Titanium release.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.