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 │ │ │
│ │ └───────────────┘ └────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Two integration paths:
- Bedrock (AI Chat): Roblox → HTTP GET → API Gateway → Lambda (Quarkus) → Amazon Bedrock → Response back to game chat
- 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
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();
}
}
}
Key points:
- We use
GETwith aquestionquery parameter — simple enough for Roblox'sHttpService:GetAsync() - Response is plain text (not JSON) — easier to display directly in Roblox chat
-
maxTokens=300keeps 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>
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
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
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"
Part 2: Roblox Scripts (Game Side)
Game Hierarchy Setup
In Roblox Studio, you need:
-
ReplicatedStorage: Two
RemoteEventobjects —AWSChatBedRockRSandAWSChatIOTRS -
ServerScriptService: Server scripts + the
Roblo3module + theAWSmodule -
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)
How it works:
- Player types
/bedrock What is serverless computing? - The
TextChatServicecommand triggers with the chat text - We extract the question (everything after "bedrock ")
- Call the Lambda API via
HttpService:GetAsync() - Fire the response to ALL connected clients via
RemoteEvent
⚠️ Important: You must enable
HttpServicein 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)
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
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)
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)
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)
🔒 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:
AWSChatBedRockRSAWSChatIOTRS
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
AWSChatClientScriptLocalScript in StarterPlayerScripts - Place the
AWSModuleScript 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
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"
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
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)