---
title: "Backend calls with Lambda and AppSync"
description: "Call backend functions and exchange data through AppSync."
seo:
  noindex: true
---

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

Use **Lambda** for a backend operation and **AppSync** for a GraphQL API with live subscriptions. Both require [AWS setup](/integrations/aws/setup) and deployed resources; these nodes do not create your backend logic.

## Call a Lambda function

**Blueprint**

<BlueprintViewer title="Load daily challenges" src="/blueprints/crossplay-aws-call-lambda.txt" />

**C++**

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

```cpp
#include "Engine/GameInstance.h"
#include "AWSIKLambda.h"
#include "Providers/IUCIKBackendFunctionProvider.h"

void LoadDailyChallenges(UGameInstance &GameInstance)
{
    const auto Backend = FAWSIKLambdaModule::Get().GetFunctionProvider();
    if (!Backend)
    {
        UE_LOG(LogAWSIKLambda, Warning, TEXT("Lambda provider unavailable"));
        return;
    }
    Backend->CallFunction(TEXT("GetDailyChallenges"), TEXT("{\"playlist\":\"duos\"}"),
                          FUCIKBackendFunctionCallback::CreateWeakLambda(
                              &GameInstance, [](const TBetideResult<FString> &Result) {
                                  if (!Result.bOk)
                                  {
                                      UE_LOG(LogAWSIKLambda, Warning, TEXT("%s"),
                                             *Result.Error.GetLogString());
                                      return;
                                  }
                                  UE_LOG(LogAWSIKLambda, Log, TEXT("%s"), *Result.GetValue());
                              }));
}
```

Deploy a function named `GetDailyChallenges` (or change **Name**) that accepts `{ "playlist": "duos" }` and returns JSON. The request must be a JSON object; empty or non-JSON responses fail. The caller needs permission to invoke that function. The helper waits for its response and reports Lambda function errors as failures. Parse the returned JSON for your own business result too; a valid response can still reject a purchase or match result. Make retried writes idempotent in your backend.

## Configure AppSync

Set **GraphQL Endpoint** and **Auth Mode** in **Betide → Crossplay → AWS AppSync**:

| Mode | Credential source |
| --- | --- |
| IAM | AWS credential chain. |
| API Key | Environment variable named by **API Key Environment Variable**. |
| Cognito User Pool | ID token in the environment variable named by **JWT Environment Variable**. |

The current Cognito mode reads that environment variable; it does not automatically take a token from the login subsystem. Leave **Realtime Endpoint** empty for a standard endpoint, or supply the WebSocket endpoint for your custom domain.

## Backend fields used below

The examples read and update `lobby-demo`. Create it in your backend, or replace the ID. The API needs these fields and matching query/mutation resolvers:

<Accordion>
  <AccordionItem title="Example AppSync schema">

```graphql
type Lobby {
  id: ID!
  status: String!
}
type Query {
  getLobby(id: ID!): Lobby
}
type Mutation {
  updateLobby(id: ID!, status: String!): Lobby
}
type Subscription {
  onUpdateLobby(id: ID!): Lobby @aws_subscribe(mutations: ["updateLobby"])
}
schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}
```

Enforce who can read or update each lobby in your backend. The mutation returns both `id` and `status` so subscribers receive both fields. See [AppSync subscription setup](https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-data.html).

  </AccordionItem>
</Accordion>

## Query AppSync

**Blueprint**

<Accordion>
  <AccordionItem title="Read lobby status">
<BlueprintViewer title="Read lobby status" src="/blueprints/crossplay-aws-appsync-query.txt" />
  </AccordionItem>
  <AccordionItem title="Update lobby status">
<BlueprintViewer title="Update lobby status" src="/blueprints/crossplay-aws-appsync-mutate.txt" />
  </AccordionItem>
</Accordion>

**C++**

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

```cpp
#include "Engine/GameInstance.h"
#include "AWSIKAppSync.h"
#include "Providers/IUCIKRealtimeDataProvider.h"

void RequestLobbyStatus(UGameInstance &GameInstance, bool bUpdate)
{
    const auto Backend = FAWSIKAppSyncModule::Get().GetProvider();
    if (!Backend)
    {
        UE_LOG(LogAWSIKAppSync, Warning, TEXT("AppSync provider unavailable"));
        return;
    }
    auto Completion = FUCIKRealtimeResultCallback::CreateWeakLambda(
        &GameInstance, [](const TBetideResult<FString> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogAWSIKAppSync, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            UE_LOG(LogAWSIKAppSync, Log, TEXT("%s"), *Result.GetValue());
        });
    if (bUpdate)
    {
        Backend->Mutate(TEXT("mutation UpdateLobby($id: ID!, $status: String!) { updateLobby(id: "
                             "$id, status: $status) { id status } }"),
                        TEXT("{\"id\":\"lobby-demo\",\"status\":\"Ready\"}"), MoveTemp(Completion));
    }
    else
    {
        Backend->Query(TEXT("query ReadLobby($id: ID!) { getLobby(id: $id) { id status } }"),
                       TEXT("{\"id\":\"lobby-demo\"}"), MoveTemp(Completion));
    }
}
```

Use `bUpdate = false` to read and `true` to set `Ready` in C++. Pass a document matching your schema and a JSON object for variables (`{}` when none). HTTP query/mutation responses with a non-empty GraphQL `errors` array fail even when HTTP succeeds.

## Subscribe and unsubscribe

**Blueprint**

<BlueprintViewer title="Watch lobby status and clean up" src="/blueprints/crossplay-aws-appsync-subscribe.txt" />

The graph keeps a **LobbySubscription** variable of type **Guid**. Call **Stop Watching Lobby Status** when closing the screen; call Watch again to restart.

**C++**

```cpp
#include "Engine/GameInstance.h"
#include "AWSIKAppSync.h"
#include "Providers/IUCIKRealtimeDataProvider.h"

void WatchLobbyStatus(UGameInstance &GameInstance, FGuid &SubscriptionId)
{
    if (SubscriptionId.IsValid())
        return;
    const auto Backend = FAWSIKAppSyncModule::Get().GetProvider();
    if (!Backend)
    {
        UE_LOG(LogAWSIKAppSync, Warning, TEXT("AppSync provider unavailable"));
        return;
    }
    const TWeakObjectPtr<UGameInstance> WeakGameInstance(&GameInstance);
    SubscriptionId = Backend->Subscribe(
        TEXT("subscription WatchLobby($id: ID!) { onUpdateLobby(id: $id) { id status } }"),
        TEXT("{\"id\":\"lobby-demo\"}"), [WeakGameInstance](const TBetideResult<FString> &Event) {
            if (!WeakGameInstance.IsValid())
                return;
            if (!Event.bOk)
            {
                UE_LOG(LogAWSIKAppSync, Warning, TEXT("%s"), *Event.Error.GetLogString());
                return;
            }
            UE_LOG(LogAWSIKAppSync, Log, TEXT("%s"), *Event.GetValue());
        });
    if (!SubscriptionId.IsValid())
        UE_LOG(LogAWSIKAppSync, Warning, TEXT("Subscription request could not start"));
}

void StopWatchingLobbyStatus(FGuid &SubscriptionId)
{
    if (const auto Backend = FAWSIKAppSyncModule::Get().GetProvider())
        Backend->Unsubscribe(SubscriptionId);
    SubscriptionId.Invalidate();
}
```

Keep `SubscriptionId` as an `FGuid` member and call `StopWatchingLobbyStatus` when the owning screen/session closes.

A valid ID means the request was tracked locally, not that the server accepted it. Updates arrive through the event; inspect the JSON payload for your data and GraphQL errors. AppSync sends updates for the linked mutation, not direct database writes.

Reconnect uses the configured backoff. The current wrapper has no connected/reconnected event. Query the latest state when opening or returning to the screen; subscriptions do not replay missed updates.
