State and Continuation Stores
Stores are embedder-supplied persistence hooks. State stores hold durable provider state. Continuation stores hold in-flight resume artifacts produced by action-required backend result envelopes.
State Store Functions
GLN_API gln_status_t GLN_CALL gln_create_state_store(
const gln_state_store_vtable_t* in_vtable,
gln_state_store_t** out_store,
gln_error_t* out_error);
GLN_API gln_status_t GLN_CALL gln_create_file_state_store(
const char* in_state_path,
const char* in_key_path_or_null,
gln_state_store_t** out_store,
gln_error_t* out_error);
GLN_API void GLN_CALL gln_destroy_state_store(gln_state_store_t* in_store);
gln_open_fints_backend, gln_open_revolut_backend, and
gln_open_wise_backend consume state stores. FinTS retains the underlying
state-store implementation during backend open, so the C store handle itself can
be destroyed after a successful FinTS open. Revolut and Wise keep non-owning
references to the C store handle; keep that handle alive until after closing
every Revolut or Wise backend that uses it.
The file-backed constructor stores an encrypted blob at in_state_path.
in_key_path_or_null selects the encryption-key sidecar path; pass NULL to
use the implementation default. Destroy the returned handle with
gln_destroy_state_store.
State Store Vtable
typedef struct {
gln_status_t (GLN_CALL *load)(
void* in_user_data,
uint8_t** out_payload,
size_t* out_len);
gln_status_t (GLN_CALL *save)(
void* in_user_data,
const uint8_t* in_payload,
size_t in_len);
void (GLN_CALL *free_payload)(
void* in_user_data,
uint8_t* in_payload);
void* user_data;
} gln_state_store_vtable_t;
Field contract:
loadreturns the previously saved blob. OnGLN_OK, write a fresh allocation to*out_payloadand its length to*out_len. A zero-length blob may useNULLwith*out_len == 0. ReturnGLN_ERR_NOT_FOUNDwhen no state has been saved yet.savepersistsin_payload[0..in_len). The buffer is library-owned and valid only during the callback. Copy or durably write it before returning.free_payloadreleases a non-null buffer previously returned byload. It must use the same allocator family thatloadused.user_datais passed back unchanged to every callback. The library does not inspect or copy it.
The direction of ownership is the practical rule: load produces a buffer that
the library later returns to free_payload; save receives a borrowed
library-owned buffer that the store must not retain.
Continuation Store Functions
GLN_API gln_status_t GLN_CALL gln_create_continuation_store(
const gln_continuation_store_vtable_t* in_vtable,
gln_secret_t* in_encryption_key,
gln_continuation_store_t** out_store,
gln_error_t* out_error);
GLN_API gln_status_t GLN_CALL gln_create_file_continuation_store(
const char* in_directory_path,
gln_secret_t* in_encryption_key,
gln_continuation_store_t** out_store,
gln_error_t* out_error);
GLN_API void GLN_CALL gln_destroy_continuation_store(gln_continuation_store_t* in_store);
gln_open_fints_backend accepts an optional continuation store. Pass NULL if
resume artifacts do not need to survive process boundaries. When a backend
result envelope contains an action-required continuation, take it with
gln_take_backend_result_continuation, save it with gln_save_continuation,
and destroy the in-memory handle when finished.
in_encryption_key is required for both constructors and must wrap exactly 32
bytes of AES-256-GCM key material. The library encrypts every continuation at
the C-API boundary; the user-supplied vtable callbacks therefore see ciphertext
(header || nonce || ciphertext || tag) rather than plaintext. The store retains
its own owning copy of the key bytes, so the caller may destroy
in_encryption_key after the constructor returns.
The file-backed continuation store writes encrypted resume blobs under
in_directory_path. Where the 32 bytes come from (sidecar file, OS keyring,
env var, password derivation) is the caller's responsibility.
Continuation Store Vtable
typedef struct {
gln_status_t (GLN_CALL *load)(
void* in_user_data,
const char* in_id,
uint8_t** out_payload,
size_t* out_len);
gln_status_t (GLN_CALL *save)(
void* in_user_data,
const char* in_id,
const uint8_t* in_payload,
size_t in_len);
void (GLN_CALL *free_payload)(
void* in_user_data,
uint8_t* in_payload);
void* user_data;
} gln_continuation_store_vtable_t;
The ownership contract is the same as the state store. The extra in_id
parameter is a library-owned string valid only during the callback. Copy it if
the backing store needs to keep it after the callback returns.
The bytes the library hands save are ciphertext (header || nonce ||
ciphertext || tag) under the AES-256-GCM key supplied at store creation. The
bytes returned from load must be the same ciphertext envelope; the library
decrypts internally before any higher layer sees the plaintext. A custom store
therefore never needs its own encryption layer.
Return GLN_ERR_NOT_FOUND when no artifact exists for in_id. Return any
non-OK status to surface load failures from the backing storage. The library
returns GLN_ERR_RESUME_ARTIFACT_INVALID itself if the loaded ciphertext fails
to decrypt or to parse, so the store implementation does not need to classify
malformed bytes.
Custom Store Skeleton
struct mem_state {
uint8_t* bytes;
size_t len;
};
static gln_status_t mem_load(
void* in_user_data,
uint8_t** out_payload,
size_t* out_len)
{
struct mem_state* state = (struct mem_state*)in_user_data;
if (state->bytes == NULL) {
return GLN_ERR_NOT_FOUND;
}
uint8_t* copy = (uint8_t*)malloc(state->len);
if (copy == NULL && state->len != 0) {
return GLN_ERR_OUT_OF_MEMORY;
}
if (state->len != 0) {
memcpy(copy, state->bytes, state->len);
}
*out_payload = copy;
*out_len = state->len;
return GLN_OK;
}
static gln_status_t mem_save(
void* in_user_data,
const uint8_t* in_payload,
size_t in_len)
{
struct mem_state* state = (struct mem_state*)in_user_data;
uint8_t* copy = NULL;
if (in_len != 0) {
copy = (uint8_t*)malloc(in_len);
if (copy == NULL) {
return GLN_ERR_OUT_OF_MEMORY;
}
memcpy(copy, in_payload, in_len);
}
free(state->bytes);
state->bytes = copy;
state->len = in_len;
return GLN_OK;
}
static void mem_free_payload(void* in_user_data, uint8_t* in_payload)
{
(void)in_user_data;
free(in_payload);
}
static struct mem_state state = { NULL, 0 };
static const gln_state_store_vtable_t vtable = {
mem_load,
mem_save,
mem_free_payload,
&state,
};
For continuation stores, add the in_id parameter to load and save and use
it as the key in the backing store.
Practical Rules
- Keep state stores profile-scoped. Sharing one state file across unrelated profiles will mix provider state.
- Keep continuation IDs narrow and explicit. Include enough application context to avoid one user's resume artifact overwriting another's.
- Complete durable writes before returning
GLN_OKfromsave. - Never retain
in_payloadaftersavereturns. - Never free a
loadbuffer directly from library code or host code; implementfree_payloadand let the library call it. - Keep
user_dataalive until no live custom store handle or retained backend store implementation can invoke its callbacks. - For Revolut and Wise state stores, destroy the C store handle only after all backends using it have been closed.
See Also
- Conventions - error, ownership, and threading rules.
- Continuations - saving, loading, and resuming artifacts.
- Key store - EBICS key-store vtable ownership.
- Troubleshooting - allocator and persistence failures.