Apollo PS4 SQLite Database Schema: Complete Guide to SAVES_DB_PATH Structure and Query Patterns
The Apollo PS4 save manager stores all user-save metadata in a single SQLite database located at /system_data/savedata/%08x/db/user/savedata.db, utilizing a fixed 19-column schema and specific INSERT, UPDATE, and SELECT query patterns executed through memory-mapped VFS operations.
Apollo PS4 is an open-source save game manager for jailbroken PlayStation 4 consoles that interacts directly with the system's savedata.db file. Understanding the Apollo PS4 SQLite database schema is critical for developers building save editing utilities or automation scripts. The database structure is defined by the PS4 operating system, but Apollo accesses it through well-defined patterns in source/saves.c and source/sqlite_db.c.
SAVES_DB_PATH File Location and Access Pattern
The database resides at a path defined by the macro SAVES_DB_PATH in include/saves.h. The concrete path expands to:
/system_data/savedata/%08x/db/user/savedata.db
Apollo substitutes the %08x placeholder with the current apollo_config.user_id to target the specific user slot. The application never creates this table; it only reads from and writes to the existing system database.
Database Schema of the savedata Table
The savedata table contains a fixed set of columns that Apollo references throughout source/saves.c. Based on the INSERT, UPDATE, and SELECT statements, the schema consists of the following fields:
| Column | Type | Description |
|---|---|---|
title_id |
TEXT | Hexadecimal game identifier (e.g., CUSA00001) |
dir_name |
TEXT | Directory name containing the actual save files |
main_title |
TEXT | Primary display title shown in the UI |
sub_title |
TEXT | Secondary title or subtitle text |
detail |
TEXT | Free-form description field |
tmp_dir_name |
TEXT | Temporary directory used during mounting operations |
is_broken |
INTEGER | Corruption flag (0 or 1) |
user_param |
INTEGER | Custom parameter used by certain patches |
blocks |
INTEGER | Number of 4 KiB blocks allocated |
free_blocks |
INTEGER | Remaining free blocks within the save |
size_kib |
INTEGER | Total size in KiB (calculated as blocks × 4) |
mtime |
TEXT | ISO-8601 timestamp of last modification |
fake_broken |
INTEGER | Internal Apollo flag for fake-broken feature |
account_id |
INTEGER | Owner's PSN account identifier |
user_id |
INTEGER | Numeric user slot (1-16) |
faked_owner |
INTEGER | Flag indicating fake account ownership |
cloud_icon_url |
TEXT | Cloud icon URL (unused on PS4) |
cloud_revision |
INTEGER | Cloud revision number |
game_title_id |
TEXT | Original title ID from the game's SFO |
A composite primary key on (title_id, dir_name) is implied by the query patterns, as Apollo always addresses specific saves using both values in WHERE clauses.
Typical Apollo Query Patterns
Apollo interacts with the database through three primary operation families, all utilizing sqlite3_mprintf for safe parameter binding and sqlite3_exec or prepared statements for execution.
Inserting New Save Entries
When creating a new save in mount mode CREATE2, Apollo executes a full INSERT statement at source/saves.c:120:
INSERT INTO savedata(title_id, dir_name, main_title, sub_title, detail, tmp_dir_name,
is_broken, user_param, blocks, free_blocks, size_kib, mtime, fake_broken,
account_id, user_id, faked_owner, cloud_icon_url, cloud_revision, game_title_id)
VALUES (%Q, %Q, '', '', '', '', 0, 0, %d, %d, %d,
strftime('%%Y-%%m-%%dT%%H:%%M:%%S.00Z', CURRENT_TIMESTAMP), 0, %ld, %d, 0, '', 0, %Q);
This pattern populates all columns, calculating size_kib from the block count and generating an ISO-8601 timestamp.
Updating Mutable Metadata
For user-initiated changes to titles or custom parameters, Apollo uses the UPDATE pattern found at source/saves.c:162:
UPDATE savedata SET (main_title, sub_title, detail, user_param) = (%Q, %Q, %Q, %ld)
WHERE (title_id=%Q AND dir_name=%Q);
This targets the composite key to modify only the display metadata without affecting file allocation.
Selecting Saves for the Browser UI
To populate the save list view, Apollo performs a minimal SELECT at source/saves.c:1572:
SELECT title_id, dir_name, main_title, blocks, account_id, sub_title
FROM savedata
This retrieves only the fields necessary for the "Save Manager" list interface, optimizing memory usage during browsing.
Retrieving Detailed Save Information
For the "Save Details" screen, Apollo queries specific fields with timestamp formatting at source/saves.c:2324:
SELECT sub_title, detail, free_blocks, size_kib, user_id, account_id, main_title,
datetime(mtime)
FROM savedata
WHERE (title_id=%Q AND dir_name=%Q)
The datetime(mtime) function converts the stored ISO-8601 string to a human-readable format for display.
Memory-Mapped Database Operations
All database interactions are abstracted through source/sqlite_db.c, which implements atomic read-modify-write cycles using the memvfs extension.
Opening the Database
The open_sqlite_db() function loads the entire database file into memory:
#include "sqlite_db.h"
#include "saves.h"
char dbpath[256];
snprintf(dbpath, sizeof(dbpath), SAVES_DB_PATH, apollo_config.user_id);
sqlite3 *db = open_sqlite_db(dbpath);
if (!db) {
LOG("Failed to open save DB at %s", dbpath);
return;
}
Source reference: source/sqlite_db.c:17
Executing a Browse Query
Listing saves follows the pattern used in the UI implementation:
sqlite3_stmt *stmt;
const char *sql = "SELECT title_id, dir_name, main_title, blocks, account_id, sub_title FROM savedata";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *tid = (const char *)sqlite3_column_text(stmt, 0);
const char *dir = (const char *)sqlite3_column_text(stmt, 1);
int blocks = sqlite3_column_int(stmt, 3);
// Populate UI list...
}
sqlite3_finalize(stmt);
}
Updating Save Metadata
Modifying a specific save requires binding to the composite key:
const char *new_sub = "Modified Subtitle";
char *sql = sqlite3_mprintf(
"UPDATE savedata SET sub_title = %Q "
"WHERE title_id = %Q AND dir_name = %Q",
new_sub, title_id, dir_name
);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
LOG("Update failed: %s", sqlite3_errmsg(db));
}
sqlite3_free(sql);
Persisting Changes Atomically
After modifications, save_sqlite_db() writes the in-memory database back to disk:
if (!save_sqlite_db(db, dbpath)) {
LOG("Failed to write database changes");
}
sqlite3_close(db);
Source reference: source/sqlite_db.c:52
This pattern ensures atomic updates on the PS4's restrictive filesystem by dumping the complete modified database in a single operation.
Summary
- Apollo PS4 SQLite database schema consists of a single
savedatatable with 19 columns including identifiers (title_id,dir_name), display metadata (main_title,sub_title), allocation metrics (blocks,free_blocks), and ownership fields (account_id,user_id). - The composite primary key
(title_id, dir_name)uniquely identifies each save entry across all query operations. - Query patterns include full INSERT statements for new saves, targeted UPDATEs for metadata modification, and selective SELECTs for UI population versus detailed views.
- Database access is abstracted through
open_sqlite_db()andsave_sqlite_db()insource/sqlite_db.c, which utilize memory-mapped VFS for atomic file operations on the PS4 system partition.
Frequently Asked Questions
How does Apollo handle database corruption or locked files?
Apollo relies on the underlying PS4 operating system to maintain database integrity. The application opens the database in read-write mode through the memvfs extension, loading the entire file into memory before operations and atomically writing it back only upon successful completion of all queries. If the system reports corruption (indicated by the is_broken column flag), Apollo displays the save as corrupted in the UI but does not attempt automatic repair.
Can I manually edit the savedata.db file while Apollo is running?
Manual editing is not recommended while Apollo is active. The application maintains an in-memory copy of the database through open_sqlite_db(), meaning any external modifications to the file on disk will be overwritten when Apollo calls save_sqlite_db() during the next save operation or application exit. To modify the database externally, ensure Apollo is completely terminated.
What is the difference between title_id and game_title_id in the schema?
According to the source code in source/saves.c, title_id represents the current identifier associated with the save mount point, while game_title_id stores the original value extracted from the game's PARAM.SFO file. In most cases these values are identical, but they may differ when using certain mounting features or when dealing with re-signed saves where the mount point differs from the original game identifier.
Where does Apollo store the database path configuration?
The database path is defined as the macro SAVES_DB_PATH in include/saves.h with the value /system_data/savedata/%08x/db/user/savedata.db. At runtime, Apollo formats this string with the current user ID from apollo_config.user_id to generate the absolute path for the specific PlayStation 4 user slot being managed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →