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 savedata table 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() and save_sqlite_db() in source/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →