---
title: "Data with DynamoDB"
description: "Read and write structured game data in DynamoDB."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Finish [AWS setup](/integrations/aws/setup). For the player-data helper, create a table with string partition key **PlayerId** and string sort key **RecordKey**, then set **DynamoDB → Player Data Table**. Restrict access by authenticated identity on the service side.

## Read a player record

Use your table name in the request. **Profile Key Json** uses DynamoDB's typed JSON:

```json
{"PlayerId":{"S":"player-demo"},"RecordKey":{"S":"Profile"}}
```

**Blueprint**

<BlueprintViewer title="Read a player record" src="/blueprints/crossplay-aws-dynamo-read-profile.txt" />

**C++**

Game module dependencies: `AWSIKDynamoDB`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

void ReadPlayerProfile(UGameInstance &GameInstance, const FString &ProfileKeyJson,
                       const FScriptDelegate &OnComplete)
{
    FAWSIKDynamoDBGetItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Key.Json = ProfileKeyJson;
    Request.bSetConsistentRead = true;
    Request.ConsistentRead = true;
    auto *Action = UAWSIKDynamoDBGetItem::GetItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKDynamoDBGetItemResult&` and `const FAWSIKDynamoDBError&`. Check `Error.IsError()` first; display `Error.Common.Message` on failure or read `Result.Item.Json` on success.

**On Success** includes missing records. Check the returned item for your key before using its data; `bSetItem` does not indicate existence. Keep **Consistent Read** enabled for a strongly consistent table read. Raw requests use **Table Name**, not the Player Data Table setting.

The player-data helper stores JSON in **Data** and a numeric **Version**. These examples use the same layout. Build dynamic keys with a JSON serializer so quotes and backslashes are escaped correctly.

## Write with a version check

These examples use `player-demo` / `Profile` in `PlayerData`. Replace those values with your own. Create stores `{"nickname":"Rookie"}` at version `1`; save changes the nickname to `Veteran` and increments the version from your last read.

**Blueprint**

<Accordion>
  <AccordionItem title="Create a profile once">
<BlueprintViewer title="Create a profile once" src="/blueprints/crossplay-aws-dynamo-create-profile.txt" />
  </AccordionItem>
  <AccordionItem title="Save a profile with its last version">
<BlueprintViewer title="Save a profile with its last version" src="/blueprints/crossplay-aws-dynamo-save-profile.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `AWSIKDynamoDB`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "AWSIKDynamoDB.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

FAWSIKDynamoDBAttributeValue DynamoAttribute(FString Json)
{
    FAWSIKDynamoDBAttributeValue Value;
    Value.Json = MoveTemp(Json);
    return Value;
}

FAWSIKDynamoDBPutItemRequest MakeDemoProfile(int64 Version, bool bCreate)
{
    FAWSIKDynamoDBPutItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Item.Add(TEXT("PlayerId"), DynamoAttribute(TEXT(R"({"S":"player-demo"})")));
    Request.Item.Add(TEXT("RecordKey"), DynamoAttribute(TEXT(R"({"S":"Profile"})")));
    Request.Item.Add(TEXT("Data"),
                     DynamoAttribute(bCreate ? TEXT(R"({"S":"{\"nickname\":\"Rookie\"}"})")
                                             : TEXT(R"({"S":"{\"nickname\":\"Veteran\"}"})")));
    Request.Item.Add(TEXT("Version"),
                     DynamoAttribute(FString::Printf(TEXT("{\"N\":\"%lld\"}"), Version)));
    Request.bSetConditionExpression = true;
    Request.bSetExpressionAttributeNames = true;
    return Request;
}

void CreatePlayerProfile(UGameInstance &GameInstance, const FScriptDelegate &OnComplete)
{
    auto Request = MakeDemoProfile(1, true);
    Request.ConditionExpression = TEXT("attribute_not_exists(#player_id)");
    Request.ExpressionAttributeNames.Add(TEXT("#player_id"), TEXT("PlayerId"));
    auto *Action = UAWSIKDynamoDBPutItem::PutItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

void SavePlayerProfile(UGameInstance &GameInstance, int64 ExpectedVersion,
                       const FScriptDelegate &OnComplete)
{
    if (ExpectedVersion <= 0 || ExpectedVersion == MAX_int64)
    {
        UE_LOG(LogAWSIKDynamoDB, Warning,
               TEXT("Expected Version must be positive and below the int64 maximum"));
        return;
    }
    auto Request = MakeDemoProfile(ExpectedVersion + 1, false);
    Request.ConditionExpression = TEXT("#version = :expected");
    Request.ExpressionAttributeNames.Add(TEXT("#version"), TEXT("Version"));
    Request.bSetExpressionAttributeValues = true;
    Request.ExpressionAttributeValues.Add(
        TEXT(":expected"),
        DynamoAttribute(FString::Printf(TEXT("{\"N\":\"%lld\"}"), ExpectedVersion)));
    auto *Action = UAWSIKDynamoDBPutItem::PutItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKDynamoDBPutItemResult&` and `const FAWSIKDynamoDBError&`. Check `Error.IsError()` and show `Error.Common.Message` on failure. On success, the saved version is `1` for create or `ExpectedVersion + 1` for save; Put Item does not return the new item.

A condition failure means the record already exists, was deleted, or has a different version. Read again and resolve the conflict before retrying. Keep the expression fields enabled on the Make Request nodes; C++ sets their `bSet...` flags.

**Put Item replaces the whole item**, including removing attributes you omit. Use Update Item for partial changes. [Put Item behavior](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html).

<Accordion>
  <AccordionItem title="Raw requests, stats, and leaderboards">

`GetPlayerDataProvider()->GetRecord` and `SetRecord` offer the same record layout through C++ callbacks. For `SetRecord`, pass `-1` to create an absent record or the version from your last read to replace one. These helpers use **Player Data Table** in settings.

The helper's `IncrementStat(PlayerId, StatName, Amount, Callback)` uses a service-side update. A retry of a successful increment can count twice. Raw **Amazon DynamoDB Update Item** supports your own update expressions; **Amazon DynamoDB Query** reads key ranges. Follow pagination tokens for raw queries.

The separate leaderboard helper expects **LeaderboardId** and a sortable string **ScoreKey** in **Leaderboard Table**, plus PlayerId, numeric Score, and Metadata fields. Your backend owns score validation and sortable-key creation. `GetLeaderboardProvider()->QueryLeaderboard(Id, Limit, Callback)` reads 1–100 entries; it does not write or maintain a ranking automatically.

  </AccordionItem>
</Accordion>

## Update and query records

The increment adds one win to `player-demo` / `stat:Wins`, creating the stat if needed. The query loads that player's `stat:` records one page at a time. Use a Boolean **StatsQueryInProgress**, default `false`, for the query graph.

**Blueprint**

<Accordion>
  <AccordionItem title="Increment a player win">
<BlueprintViewer title="Increment a player win" src="/blueprints/crossplay-aws-dynamo-increment-wins.txt" />
  </AccordionItem>
  <AccordionItem title="Load every stat page">
<BlueprintViewer title="Load every stat page" src="/blueprints/crossplay-aws-dynamo-query-stats.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `AWSIKDynamoDB`, `BetideCore`, `Engine`.

<Accordion>
  <AccordionItem title="Increment wins">

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

void RecordPlayerWin(UGameInstance &GameInstance, const FScriptDelegate &OnComplete)
{
    FAWSIKDynamoDBUpdateItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Key.Json = TEXT(R"({"PlayerId":{"S":"player-demo"},"RecordKey":{"S":"stat:Wins"}})");
    Request.bSetUpdateExpression = true;
    Request.UpdateExpression =
        TEXT("ADD #value :one SET #version = if_not_exists(#version, :zero) + :one");
    Request.bSetReturnValues = true;
    Request.ReturnValues = EAWSIKDynamoDBReturnValue::ALL_NEW;
    Request.bSetExpressionAttributeNames = true;
    Request.ExpressionAttributeNames.Add(TEXT("#value"), TEXT("Value"));
    Request.ExpressionAttributeNames.Add(TEXT("#version"), TEXT("Version"));
    Request.bSetExpressionAttributeValues = true;
    FAWSIKDynamoDBAttributeValue One;
    One.Json = TEXT(R"({"N":"1"})");
    FAWSIKDynamoDBAttributeValue Zero;
    Zero.Json = TEXT(R"({"N":"0"})");
    Request.ExpressionAttributeValues.Add(TEXT(":one"), MoveTemp(One));
    Request.ExpressionAttributeValues.Add(TEXT(":zero"), MoveTemp(Zero));
    auto *Action = UAWSIKDynamoDBUpdateItem::UpdateItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKDynamoDBUpdateItemResult&` and `const FAWSIKDynamoDBError&`. Check `Error.IsError()` first, then read `Result.Attributes.Json` for the updated stat.

  </AccordionItem>
  <AccordionItem title="Read every stat page">

Add these members to your Game Instance class, or use this class as the project's Game Instance.

```cpp
// DynamoStatsGameInstance.h
#pragma once

#include "Engine/GameInstance.h"
#include "Generated/Tables/Nodes/AWSIKDynamoDBTablesNodes02.h"
#include "DynamoStatsGameInstance.generated.h"

UCLASS()
class UDynamoStatsGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void LoadPlayerStats();

  private:
    bool bStatsQueryInProgress = false;
    void ReadStatsPage(const FString &StartKeyJson);
    UFUNCTION()
    void OnStatsPage(const FAWSIKDynamoDBQueryResult &Result, const FAWSIKDynamoDBError &Error);
};
```

```cpp
// DynamoStatsGameInstance.cpp
#include "DynamoStatsGameInstance.h"
#include "AWSIKDynamoDB.h"

void UDynamoStatsGameInstance::LoadPlayerStats()
{
    if (bStatsQueryInProgress)
    {
        UE_LOG(LogAWSIKDynamoDB, Warning, TEXT("Stats query already running"));
        return;
    }
    bStatsQueryInProgress = true;
    ReadStatsPage(TEXT("{}"));
}

void UDynamoStatsGameInstance::ReadStatsPage(const FString &StartKeyJson)
{
    FAWSIKDynamoDBQueryRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.bSetLimit = true;
    Request.Limit = 25;
    Request.bSetExclusiveStartKey = true;
    Request.ExclusiveStartKey.Json = StartKeyJson;
    Request.bSetKeyConditionExpression = true;
    Request.KeyConditionExpression = TEXT("#player = :player AND begins_with(#record, :prefix)");
    Request.bSetExpressionAttributeNames = true;
    Request.ExpressionAttributeNames.Add(TEXT("#player"), TEXT("PlayerId"));
    Request.ExpressionAttributeNames.Add(TEXT("#record"), TEXT("RecordKey"));
    Request.bSetExpressionAttributeValues = true;
    FAWSIKDynamoDBAttributeValue Player;
    Player.Json = TEXT(R"({"S":"player-demo"})");
    FAWSIKDynamoDBAttributeValue Prefix;
    Prefix.Json = TEXT(R"({"S":"stat:"})");
    Request.ExpressionAttributeValues.Add(TEXT(":player"), MoveTemp(Player));
    Request.ExpressionAttributeValues.Add(TEXT(":prefix"), MoveTemp(Prefix));
    auto *Action = UAWSIKDynamoDBQuery::Query(this, Request);
    Action->OnSuccess.AddDynamic(this, &UDynamoStatsGameInstance::OnStatsPage);
    Action->OnFailure.AddDynamic(this, &UDynamoStatsGameInstance::OnStatsPage);
    Action->Activate();
}

void UDynamoStatsGameInstance::OnStatsPage(const FAWSIKDynamoDBQueryResult &Result,
                                           const FAWSIKDynamoDBError &Error)
{
    if (Error.IsError())
    {
        bStatsQueryInProgress = false;
        UE_LOG(LogAWSIKDynamoDB, Warning, TEXT("%s"), *Error.Common.Message);
        return;
    }
    UE_LOG(LogAWSIKDynamoDB, Log, TEXT("%s"), *Result.Items.Json);
    if (Result.LastEvaluatedKey.Json.Contains(TEXT("\"PlayerId\":"), ESearchCase::CaseSensitive))
    {
        ReadStatsPage(Result.LastEvaluatedKey.Json);
        return;
    }
    bStatsQueryInProgress = false;
    UE_LOG(LogAWSIKDynamoDB, Log, TEXT("All stat pages loaded"));
}
```

  </AccordionItem>
</Accordion>

An increment is atomic, but repeating it can count a win twice. For rewards that must happen once, use a backend operation with duplicate detection. [Atomic counter behavior](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters).

The query checks for `"PlayerId":` in the plugin's compact **Last Evaluated Key Json**; change that check if you rename the partition key. `bSetLastEvaluatedKey` is always set by the current converter, so it cannot tell you whether another page exists. Pass the entire returned key unchanged. An empty item page can still have a continuation key. [Query pagination](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.Pagination.html).

Queries are eventually consistent by default and pages are not a snapshot. Change the sample player ID in both examples and enforce access in your runtime role or backend.
