Persistence & Sessions
cap2UI5 apps are stateful: their fields survive a roundtrip, a browser refresh, sometimes even a server restart. This page explains how persistence works and where its limits are.
The persistence table
In db/schema.cds:
namespace my.domain;
entity z2ui5_t_01 {
key id : UUID;
id_prev : UUID; // ← predecessor ID, builds the history
data : LargeString; // ← serialized app instance
}On every roundtrip:
- The app instance is serialized (
z2ui5_cl_core_srv_draft.serialize). - A new UUID is generated.
- An
INSERTis made intoz2ui5_t_01—id_prevpoints to the previous instance. - The new UUID goes back in the response (
S_FRONT.ID). - The frontend sends this ID along on the next roundtrip.
- The server loads the instance again and applies the XX delta before
main().
This is an append-only history. If you want, you can traverse the past via id_prev — for example, to build an "undo" mechanism.
What gets serialized?
The engine walks Object.getOwnPropertyNames(oApp) and includes anything that:
- Is not a function
- Is not in the
SKIP_PROPSset (["client"]) - Is JSON-serializable
That means: your data fields survive, bound methods and closures do not.
class my_app extends z2ui5_if_app {
username = "Alice"; // ✓ persisted
preferences = { lang: "de" }; // ✓ persisted
computed = null; // ✓ persisted (even when null)
client = null; // ← skipped (in SKIP_PROPS)
helper = () => { … }; // ✗ function — not persisted
__cache = new Map(); // ✗ Map is not JSON round-trippable
conn = await cds.connect.to(...); // ✗ connection object
}Rule of thumb
Keep app fields JSON pure: strings, numbers, booleans, arrays, plain objects. If you need maps, sets, connections, or streams, declare them as local variables in main() — they then live for exactly one roundtrip.
Class restoration
Serialization writes two meta fields into the output:
{
"__className": "my_app",
"__filePath": "../../samples/my_app.js",
"username": "Alice",
/* ... */
}On deserialization, __filePath is resolved and require()d, then a new instance is created and populated with Object.assign.
__filePath is determined via _findAppFile(className). It currently searches:
srv/z2ui5/02/<className>.jssrv/z2ui5/02/01/<className>.jssrv/samples/<className>.js
Keep your apps in the three paths
If you want to put apps somewhere else, you'll need to extend _findAppFile in z2ui5_cl_core_srv_draft.js, otherwise the reload fails. In practice, srv/samples/ is usually the simplest path.
Database backend
CAP-typically this is covered by your cds.requires.db driver:
@cap-js/sqlitein dev (npx cds wautomatically starts an in-memory SQLite)- HANA / HANA Cloud in prod
- PostgreSQL via
@cap-js/postgres
The engine uses only INSERT.into(...) and SELECT.one.from(...) — all CDS service backends work.
Cleanup
Since every roundtrip writes a new entry into z2ui5_t_01, the table grows linearly. In production you need a cleanup strategy. Two ways:
1. CAP periodic job (simple):
// srv/cleanup.js
const cds = require("@sap/cds");
cds.on("served", () => {
setInterval(async () => {
const { z2ui5_t_01 } = cds.entities("my.domain");
const cutoff = new Date(Date.now() - 24*60*60*1000).toISOString();
await DELETE.from(z2ui5_t_01).where(`createdAt < '${cutoff}'`);
}, 60*60*1000);
});(Assumes you have enabled @cds.persistence.journal or the cuid aspect with createdAt, or are logging the time yourself in data.)
2. DB-side job with your DB operator tooling, e.g. a nightly cron on HANA that deletes older rows.
There is currently no built-in cleanup — that is intentional, because the right strategy is project-specific.
Sticky sessions
Some apps need to ensure that roundtrips arrive strictly serially — e.g. a wizard where step 2 must never finish before step 1:
this.check_sticky = true;
client.set_session_stateful(true);This sets a flag on the frontend that waits on the next click until the previous roundtrip is done — preventing race conditions for fast clickers.
What happens on server restart?
- Apps in the DB survive (they're persisted).
- In-flight promises are lost (logical).
- The frontend doesn't notice — it sends its ID as usual, and the server reloads the app from the DB.
Meaning: cap2UI5 apps are inherently stateless on the server level (in the sense of "no in-memory state per user"). You can scale them horizontally, as long as all instances share the same DB backend.
Performance tips
- Keep app instances small: persist only what you really need for subsequent roundtrips. Reload database results fresh in
check_on_init()instead of caching them in the app. - Avoid huge arrays as fields: 10,000 rows in
this.usersmeans 10,000 rows per roundtrip in the DB — that adds up. - Use backed queries for tables: bind
itemsto an OData service viaset_odata_modelrather than to an app array.
→ Continue with Popups & Toasts.