DEV Community

Vinicius Senger
Vinicius Senger

Posted on

Integrating Roblox with Amazon Bedrock and DynamoDB — Building an AI-Powered Game World

What if players in your Roblox game could chat with an AI directly inside the game? Or trigger real-world IoT devices just by sitting on a virtual chair?

In this tutorial, I'll show you how I integrated Roblox with AWS services — specifically Amazon Bedrock (for generative AI) and Amazon DynamoDB (for IoT command/sensor data) — creating an interactive experience where the virtual and physical worlds connect.

The result is a Roblox game where:

  • Players can ask questions to an AI (Amazon Nova Lite) via in-game chat commands
  • NPCs trigger real-world IoT actions through DynamoDB and MQTT
  • Sensor data from the physical world appears inside the game

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                      ROBLOX GAME                            │
│                                                             │
│  ┌───────────┐   ┌────────────────┐   ┌──────────────┐      │
│  │LocalScript│◄──│ ServerScripts  │──►│ Roblo3 SDK   │      │
│  │ (Chat UI) │   │(Chat Commands) │   │ (DynamoDB)   │      │
│  └───────────┘   └────────┬───────┘   └──────┬───────┘      │
│                           │                  │              │
└───────────────────────────┼──────────────────┼──────────────┘
                            │ HTTP GET         │ AWS API
                            ▼                  ▼
┌─────────────────────────────────────────────────────────────┐
│                        AWS CLOUD                            │
│                                                             │
│  ┌───────────┐   ┌──────────────┐   ┌─────────────────┐     │
│  │API Gateway│──►│ AWS Lambda   │──►│ Amazon Bedrock  │     │
│  │(HTTP API) │   │(Quarkus/Java)│   │  (Nova Lite)    │     │
│  └───────────┘   └──────────────┘   └─────────────────┘     │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                Amazon DynamoDB                      │    │
│  │  ┌───────────────┐       ┌────────────────┐         │    │
│  │  │gaming_commands│       │ gaming_sensors │         │    │
│  │  └───────────────┘       └────────────────┘         │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Two integration paths:

  1. Bedrock (AI Chat): Roblox → HTTP GET → API Gateway → Lambda (Quarkus) → Amazon Bedrock → Response back to game chat
  2. DynamoDB (IoT): Roblox → Roblo3 SDK → DynamoDB direct API calls → Read sensors / Write commands

Prerequisites

  • Roblox Studio installed
  • AWS Account with access to Amazon Bedrock (Nova Lite model enabled)
  • AWS SAM CLI installed
  • Java 17 and Maven installed
  • Basic knowledge of Lua (Roblox scripting) and Java

Part 1: The AWS Lambda Backend (Bedrock Integration)

Step 1: Create the Quarkus Project

We use Quarkus with the quarkus-amazon-lambda-http extension to create a serverless REST API that wraps Amazon Bedrock.

roblox-bedrock/
├── pom.xml
├── template.yml          (SAM deployment template)
├── samconfig.toml        (SAM configuration)
└── src/main/java/org/acme/
    └── BedrockResource.java
Enter fullscreen mode Exit fullscreen mode

Step 2: The Bedrock REST Endpoint

Here's the core Java class that receives questions from Roblox and forwards them to Amazon Bedrock's Converse API using the Amazon Nova Lite model:

package org.acme;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import software.amazon.awssdk.services.bedrockruntime.model.*;

@Path("/bedrock")
public class BedrockResource {

    private static final String MODEL_ID = "us.amazon.nova-lite-v1:0";
    private final BedrockRuntimeClient bedrockClient;

    public BedrockResource() {
        this.bedrockClient = BedrockRuntimeClient.create();
    }

    @GET
    @Path("/ask")
    @Produces(MediaType.TEXT_PLAIN)
    public String ask(@QueryParam("question") String question) {
        if (question == null || question.isBlank()) {
            return "Please provide a question via ?question=...";
        }
        return askBedrock(question);
    }

    private String askBedrock(String question) {
        try {
            Message userMessage = Message.builder()
                .role(ConversationRole.USER)
                .content(ContentBlock.fromText(question))
                .build();

            InferenceConfiguration inferenceConfig = InferenceConfiguration.builder()
                .maxTokens(300)
                .temperature(0.7f)
                .topP(1f)
                .build();

            ConverseRequest request = ConverseRequest.builder()
                .modelId(MODEL_ID)
                .messages(userMessage)
                .inferenceConfig(inferenceConfig)
                .build();

            ConverseResponse response = bedrockClient.converse(request);
            return response.output().message().content().get(0).text();
        } catch (Exception e) {
            return "Error: " + e.getMessage();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • We use GET with a question query parameter — simple enough for Roblox's HttpService:GetAsync()
  • Response is plain text (not JSON) — easier to display directly in Roblox chat
  • maxTokens=300 keeps responses concise for in-game display
  • We use the Converse API which is the recommended way to interact with Bedrock models

Step 3: Maven Dependencies

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-amazon-lambda-http</artifactId>
    </dependency>
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bedrockruntime</artifactId>
        <version>2.29.29</version>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

The quarkus-amazon-lambda-http extension automatically adapts your REST endpoints to work behind API Gateway as a Lambda function — no code changes needed between local dev and production.

Step 4: SAM Template for Deployment

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Roblox Quarkus AWS SAM application

Resources:
  RobloxBedrock:
    Type: AWS::Serverless::Function
    Properties:
      Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
      Runtime: java17
      CodeUri: target/function.zip
      MemorySize: 1024
      SnapStart:
        ApplyOn: PublishedVersions
      AutoPublishAlias: snap
      Timeout: 15
      Policies:
        - Statement:
            - Sid: bedrockall
              Effect: Allow
              Action:
                - bedrock:*
              Resource: '*'
      Environment:
        Variables:
          JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
      Events:
        HttpApiEvent:
          Type: HttpApi

Outputs:
  RobloxBedrock:
    Description: URL for application
    Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/'
    Export:
      Name: RobloxBedrock
Enter fullscreen mode Exit fullscreen mode

Key deployment features:

  • SnapStart — dramatically reduces Lambda cold start times for Java
  • HttpApi — uses API Gateway HTTP API (v2) for lower latency and cost
  • bedrock:* — allows the Lambda to call any Bedrock model
  • 15-second timeout — Bedrock inference can take a few seconds

Step 5: Build and Deploy

# Build the project
./mvnw clean package -DskipTests

# Deploy with SAM
sam build
sam deploy --guided
Enter fullscreen mode Exit fullscreen mode

After deployment, you'll get an API Gateway URL. Test it:

curl "https://<your-api-id>.execute-api.us-east-1.amazonaws.com/bedrock/ask?question=What+is+cloud+computing"
Enter fullscreen mode Exit fullscreen mode

Part 2: Roblox Scripts (Game Side)

Game Hierarchy Setup

In Roblox Studio, you need:

  • ReplicatedStorage: Two RemoteEvent objects — AWSChatBedRockRS and AWSChatIOTRS
  • ServerScriptService: Server scripts + the Roblo3 module + the AWS module
  • StarterPlayerScripts: The client-side AWSChatClientScript
  • TextChatService: Custom chat commands (chatAWSBedrock, chatAWSIoT, chatAWSConfig)

Script 1: Bedrock Chat ServerScript

This server script listens for the /bedrock chat command, calls the Lambda API, and broadcasts the AI response to all players:

print("AWS Config initializing")
DataStoreService = game:WaitForChild("DataStoreService")
awsCredentialStore = DataStoreService:WaitForChild("AwsCredential")

local textChatService = game:GetService("TextChatService")
local commandInstance1 = textChatService["chatAWSBedrock"]

commandInstance1.Triggered:Connect(function(plr, chat)
    -- Extract the question from the chat command
    -- Command format: "bedrock <question>"
    local url = "https://<your-api-id>.execute-api.us-east-1.amazonaws.com/"
    local http = game:GetService("HttpService")
    local param = string.sub(chat, 9, string.len(chat))

    print(url .. "bedrock/ask?question=" .. param)
    local httpResponse = http:GetAsync(url .. "bedrock/ask?question=" .. param)
    print(httpResponse)

    -- Broadcast response to all connected clients
    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local publisher = ReplicatedStorage:WaitForChild("AWSChatBedRockRS")
    publisher:FireAllClients(httpResponse)
end)
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. Player types /bedrock What is serverless computing?
  2. The TextChatService command triggers with the chat text
  3. We extract the question (everything after "bedrock ")
  4. Call the Lambda API via HttpService:GetAsync()
  5. Fire the response to ALL connected clients via RemoteEvent

⚠️ Important: You must enable HttpService in your Roblox game settings (Game Settings → Security → Allow HTTP Requests).

Script 2: Client-Side Display (AWSChatClientScript)

This LocalScript runs on each player's client and displays the AI responses in the chat:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local listenerIoT = ReplicatedStorage:WaitForChild("AWSChatIOTRS")
local listenerBedrock = ReplicatedStorage:WaitForChild("AWSChatBedRockRS")
print("setup client")

-- Display Bedrock AI responses in chat
listenerBedrock.OnClientEvent:Connect(function(text)
    game.TextChatService.TextChannels.RBXGeneral:DisplaySystemMessage(text)
end)

-- Display IoT sensor data in chat
listenerIoT.OnClientEvent:Connect(function(text)
    game.TextChatService.TextChannels.RBXGeneral:DisplaySystemMessage(
        "The room temperature is " .. text
    )
end)
Enter fullscreen mode Exit fullscreen mode

Part 3: DynamoDB Integration (IoT Commands & Sensors)

The AWS Module (ModuleScript)

This reusable module handles all DynamoDB interactions using the Roblo3 library (an AWS SDK implementation for Roblox/Lua):

local module = {}
local ServerScriptService = game:WaitForChild("ServerScriptService")
local roblo3 = require(ServerScriptService.Roblo3)
DataStoreService = game:WaitForChild("DataStoreService")
awsCredentialStore = DataStoreService:GetDataStore("AwsCredential")

-- Load AWS credentials from Roblox DataStore
local setSuccess, errorMessage = pcall(function()
    region = awsCredentialStore:GetAsync("REGION")
    accessKey = awsCredentialStore:GetAsync("ACCESS_KEY")
    secretKey = awsCredentialStore:GetAsync("SECRET_KEY")
end)

if not setSuccess then
    warn(errorMessage)
end

local awsArgs = {
    ["regionName"] = region,
    ["accessKeyId"] = accessKey,
    ["secretAccessKey"] = secretKey
}

-- Initialize DynamoDB tables
local dynamodb = roblo3.resource("dynamodb", awsArgs)
local table_commands = dynamodb:Table("gaming_commands")
local table_sensors = dynamodb:Table("gaming_sensors")

-- Read sensor data from DynamoDB
function module.getSensor(sensor)
    local response_note = table_sensors:GetItem({
        ["Key"] = {
            ["pk"] = "sensor",
            ["sk"] = sensor
        }
    })
    return response_note
end

-- Write IoT commands to DynamoDB
function module.publish_command(command)
    local response = table_commands:PutItem({
        ["Item"] = {
            ["command"] = command,
            ["timestamp"] = os.time()
        }
    })
    print(response)
end

print("AWS Service Initialized...")
return module
Enter fullscreen mode Exit fullscreen mode

The IoT Chat ServerScript

Players can query sensors and control IoT devices via chat commands:

print("Chat initializing")
local ServerScriptService = game:GetService("ServerScriptService")
local roblo3 = require(ServerScriptService.Roblo3)
DataStoreService = game:GetService("DataStoreService")
awsCredentialStore = DataStoreService:GetDataStore("AwsCredential")

-- Load credentials
local setSuccess, errorMessage = pcall(function()
    region = awsCredentialStore:GetAsync("REGION")
    accessKey = awsCredentialStore:GetAsync("ACCESS_KEY")
    secretKey = awsCredentialStore:GetAsync("SECRET_KEY")
end)

local awsArgs = {
    ["regionName"] = region,
    ["accessKeyId"] = accessKey,
    ["secretAccessKey"] = secretKey
}

local dynamodb = roblo3.resource("dynamodb", awsArgs)
local table_sensors = dynamodb:Table("gaming_sensors")
local table_commands = dynamodb:Table("gaming_commands")

local textChatService = game:GetService("TextChatService")
local commandInstance1 = textChatService["chatAWSIoT"]

commandInstance1.Triggered:Connect(function(plr, chat)
    -- /iot temperature - read sensor data
    if chat:split(" ")[1] == "iot" and chat:split(" ")[2] == "temperature" then
        local response = table_sensors:GetItem({
            ["Key"] = {
                ["pk"] = "sensor",
                ["sk"] = "temperature"
            }
        })
        local ReplicatedStorage = game:GetService("ReplicatedStorage")
        local publisher = ReplicatedStorage:WaitForChild("AWSChatIOTRS")
        publisher:FireAllClients(response.value)
    end

    -- /iot lamp on|off - control IoT device
    if chat:split(" ")[1] == "iot" and chat:split(" ")[2] == "lamp" then
        local response = table_commands:PutItem({
            ["Item"] = {
                ["command"] = "command lamp-" .. chat:split(" ")[3],
                ["timestamp"] = os.time()
            }
        })
        print(response)
    end
end)
Enter fullscreen mode Exit fullscreen mode

NPC & Game Object Triggers

Game objects can also trigger AWS commands. For example, sitting in a chair triggers an IoT action:

local seat = script.Parent
local ServerScriptService = game:WaitForChild("ServerScriptService")
local aws = require(ServerScriptService.AWS)
local ChatService = game:GetService("Chat")
local npc = script.Parent.Parent.Parent.npcCode
local players = game:GetService("Players")

seat:GetPropertyChangedSignal("Occupant"):Connect(function()
    if seat.Occupant then
        local character = seat.Occupant.Parent
        local player = players:GetPlayerFromCharacter(character)
        local points = player.leaderstats.Points
        points.Value += 10

        -- Trigger real-world IoT action via DynamoDB/MQTT
        aws.publish_command("mqtt-publish|command|audio_kiro_code1")

        ChatService:Chat(npc.Ghost.Body, "Let's try Kiro!", Enum.ChatColor.White)
        wait(2)
        aws.publish_command("mqtt-publish|command|vscode_kiro_intro")
    end
end)
Enter fullscreen mode Exit fullscreen mode

Part 4: AWS Credential Management

Credentials for DynamoDB direct access are configured through an in-game chat command:

print("AWS Config initializing")
DataStoreService = game:GetService("DataStoreService")
awsCredentialStore = DataStoreService:GetDataStore("AwsCredential")

local textChatService = game:GetService("TextChatService")
local commandInstance1 = textChatService["chatAWSConfig"]

commandInstance1.Triggered:Connect(function(plr, chat)
    -- Command format: aws config <region> <access_key> <secret_key>
    if chat:split(" ")[2] == "config" then
        local region = chat:split(" ")[3]
        local access = chat:split(" ")[4]
        local secret = chat:split(" ")[5]

        local setSuccess, errorMessage = pcall(function()
            awsCredentialStore:SetAsync("REGION", region)
            awsCredentialStore:SetAsync("ACCESS_KEY", access)
            awsCredentialStore:SetAsync("SECRET_KEY", secret)
        end)

        if not setSuccess then
            warn(errorMessage)
        end
    end
end)
Enter fullscreen mode Exit fullscreen mode

🔒 Security Note: In production, use IAM roles with minimal permissions and consider a proxy service instead of embedding credentials. The DataStore approach keeps credentials server-side only (never exposed to clients), but you should rotate them regularly and scope them to specific DynamoDB tables.


Part 5: DynamoDB Table Design

gaming_sensors Table

Attribute Type Description
pk String Partition key ("sensor")
sk String Sort key (sensor name)
value String Current sensor reading

gaming_commands Table

Attribute Type Description
command String Command string (e.g., "lamp-on")
timestamp Number Unix epoch timestamp

The command format supports MQTT routing: mqtt-publish|<topic>|<payload> — a separate IoT microservice polls this table and publishes to the appropriate MQTT topics.


Part 6: Setting Up in Roblox Studio

1. Enable HTTP Requests
Go to Game Settings → Security → Allow HTTP Requests → Enable

2. Create RemoteEvents
In ReplicatedStorage, create two RemoteEvent instances:

  • AWSChatBedRockRS
  • AWSChatIOTRS

3. Set Up TextChatService Commands
In TextChatService, create TextChatCommand objects:

  • chatAWSBedrock — triggered by /bedrock
  • chatAWSIoT — triggered by /iot
  • chatAWSConfig — triggered by /aws

4. Add the Roblo3 Library
The Roblo3 library provides AWS SDK functionality for Roblox Lua. Place it under ServerScriptService as a ModuleScript with its DynamoDB sub-module.

5. Deploy Scripts

  • Place server scripts in ServerScriptService
  • Place the AWSChatClientScript LocalScript in StarterPlayerScripts
  • Place the AWS ModuleScript in ServerScriptService

How It All Comes Together

AI Chat Flow

Player types: /bedrock What is Amazon Bedrock?
  → ServerScript catches command
  → HttpService:GetAsync(API Gateway URL)
  → API Gateway routes to Lambda
  → Quarkus Lambda calls Bedrock Converse API (Nova Lite)
  → AI response returned as plain text
  → ServerScript fires RemoteEvent to all clients
  → All players see the AI response in chat
Enter fullscreen mode Exit fullscreen mode

IoT Sensor Flow

Player types: /iot temperature
  → ServerScript catches command
  → Roblo3 DynamoDB:GetItem on gaming_sensors table
  → Returns sensor value
  → Fires RemoteEvent to all clients
  → Players see "The room temperature is 23.5°C"
Enter fullscreen mode Exit fullscreen mode

Physical World Trigger Flow

Player sits in a chair in the game
  → Seat.Occupant change detected
  → aws.publish_command("mqtt-publish|command|audio_kiro_code1")
  → DynamoDB PutItem on gaming_commands table
  → External IoT service polls table
  → MQTT message published to physical device
  → Real-world speaker plays audio / VS Code opens
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Component Optimization
Lambda cold start SnapStart + Tiered compilation
Lambda memory 1024MB — balances cost vs. speed
API Gateway HTTP API (v2) — lower latency than REST API
Response size maxTokens=300 — keeps responses game-friendly
DynamoDB Direct SDK calls via Roblo3 — no Lambda middleman

Lessons Learned

1. Keep it simple — Using GET with a query parameter makes Roblox integration trivial. HttpService:GetAsync() is all you need.

2. Plain text responses — Returning plain text instead of JSON eliminates the need for JSON parsing on the Roblox side.

3. SnapStart is essential — Java Lambda cold starts can be 5-10 seconds without it. With SnapStart, response times drop to ~1-2 seconds.

4. Two integration patterns work well:

  • HTTP → Lambda → Bedrock: For AI inference (needs compute, IAM)
  • Direct DynamoDB via Roblo3: For simple CRUD (lower latency, no Lambda)

5. RemoteEvents for broadcasting — Firing to all clients means everyone in the game sees the AI response — creating a shared experience.

6. IoT bridge via DynamoDB — Using DynamoDB as a command queue is simple and reliable. A separate service polls for new commands and publishes via MQTT.


Next Steps

  • Add conversation memory by storing chat history in DynamoDB
  • Implement per-player AI sessions with context
  • Add streaming responses using WebSockets
  • Create visual indicators while the AI is "thinking"
  • Add rate limiting to prevent API abuse
  • Implement a proper authentication flow instead of DataStore credentials

Conclusion

Integrating Roblox with AWS services opens up incredible possibilities for creating immersive, intelligent game experiences. By combining Amazon Bedrock's generative AI with DynamoDB's real-time data capabilities, we created a game where:

  • Players can have AI conversations without leaving the game
  • Virtual actions trigger real-world IoT devices
  • Real-world sensor data flows into the virtual world

The architecture is intentionally simple — a single Lambda function, two DynamoDB tables, and a handful of Lua scripts — but the player experience feels magical. That's the power of serverless: minimal infrastructure, maximum impact.

Happy building!

Top comments (0)