Praxsuite

Implementación del SDK de Lua en Roblox

Mirko Franichevic · 27 de agosto de 2026

¿Qué Vamos a Hacer?

Vas a conectar un juego de Roblox a un backend real: leyendo y escribiendo filas en una base de datos en la nube, y llamando a lógica del servidor que tus jugadores nunca pueden ver ni alterar.

Al final de esta guía sabrás:

  • Qué es un workspace de Praxsuite y dónde encaja tu juego en él

  • Qué credencial usar y por qué la incorrecta es un problema de seguridad

  • Cómo instalar e inicializar el SDK (Software Development Kit)

  • Cómo identificar a un jugador, leer y escribir datos, y llamar a un Gateway Endpoint

Nivel requerido: Debes sentirte cómodo escribiendo Luau en Roblox Studio. No necesitas experiencia previa con APIs o backends.


¿Qué es Praxsuite?

Piensa en Praxsuite como una base de datos en la nube con la que tu juego habla por internet, y con una capa programable encima.

Un Workspace es la carpeta grande que guarda todo lo que construyes. Dos piezas importan aquí: una Table son filas y columnas que tu juego lee y escribe, y un Endpoint es lógica que escribiste y que tu juego solo puede pedir - no puede ver lo que hay dentro.

Esa diferencia es todo el punto. Cualquier cosa que un tramposo querría cambiar pertenece a un Endpoint.

¿Qué es un Gateway? La puerta de entrada de tu workspace: la única dirección HTTPS por la que pasa cada petición. Revisa tu credencial, aplica límites de uso y pasa la petición. Tu juego nunca habla directo con la base de datos.


Requisitos Previos

Requisito

Descripción

Roblox Studio

Cualquier versión reciente. Vas a usar ServerScriptService y Game Settings.

Un workspace de Praxsuite

Créalo en portal.praxsuite.com. El plan gratuito alcanza para esta guía.

Una Table

Crea una Table llamada player_profiles con las columnas roblox_id (ShortText) y coins (Integer).

Peticiones HTTP habilitadas

En Studio: Game Settings → Security → Allow HTTP Requests. Sin esto, toda llamada falla.

Un place publicado

Solo hace falta para la ruta de producción del Step 2. Con Studio solo alcanza mientras sigues la guía.

Importante: Roblox bloquea HTTP desde Studio hasta que lo activas por place. Si tu primera llamada falla con `Http requests are not enabled`, esta es la razón.


Cómo Obtener tus Credenciales

Dos valores identifican a tu juego ante Praxsuite. Necesitas ambos.

Tu Workspace ID

Abre tu workspace en el portal y mira la barra de direcciones. El UUID (identificador único universal) que está después de /workspace/ es tu workspace ID:

https://portal.praxsuite.com/workspace/ffd80539-a1e2-4a9e-8b33-f716bf690281
                                       └──────── este es tu workspaceId ────────┘

Si todavía no abriste el workspace, el selector del menú principal del portal lista todos los workspaces a los que perteneces con su UUID al lado del nombre, tanto en vista de lista como de grilla.

prax-roblox-2.png

Tu Key: publishable vs secret

El Gateway emite dos tipos de key, y no son intercambiables.

Key

Prefijo

Dónde puede vivir

Publishable

pk_live_

En cualquier lado, incluso en código que un jugador puede leer. Está pensada para ser pública.

Secret

sk_live_

Solo del lado del servidor. Da lo que la key tenga alcance para hacer.

En Roblox el SDK corre en ServerScriptService, que los jugadores no pueden leer, así que una secret key es la elección correcta.

Regla de oro: Nunca pongas una secret key en un LocalScript, en ReplicatedStorage ni en ningún Instance que se replique al cliente. Si un jugador puede verla, ya no es un secreto.

Créala en Gateway → Credentials y limítala solo a las tables que este juego necesita. Una key limitada a una table no puede tocar el resto de tu workspace aunque se filtre.

imagen_2026-08-28_092355323.png

Step 1 - Instalar el SDK

Repositorio: https://github.com/TesseractSoftwares/Praxsuite-SDK-Lua

Descarga PraxsuiteSDK.rbxm desde la página de Releases del repositorio. En Roblox Studio, haz clic derecho en ServerScriptService → Insert from File → elige el archivo.

prax-roblox-4.png

Tu Explorer debería quedar así:

ServerScriptService
└── PraxsuiteSDK          (ModuleScript)
    ├── Core
    │   ├── Config
    │   ├── Http
    │   └── PraxQL
    ├── Data
    ├── Endpoints
    ├── Players
    └── Schema

Si usas Rojo, apúntalo a la carpeta src/ del repositorio - el resultado es el mismo.

Importante: El SDK no está publicado en Wally. Una versión vieja de la documentación decía `tesseract/praxsuite-sdk`; ese paquete nunca existió. Usa el `.rbxm` o Rojo.

¿Por qué ServerScriptService y no ReplicatedStorage?

Todo lo que está en ReplicatedStorage se copia al dispositivo de cada jugador, donde puede leerlo con la consola de desarrollo. El SDK guarda tu credencial. ServerScriptService nunca se replica, así que un cliente modificado no se entera de nada.


Step 2 - Crear el Cliente

El SDK es un singleton: lo configuras una vez, y cualquier otro script obtiene la misma instancia configurada con solo hacerle require.

Hay dos formas de configurarlo. Empieza por la explícita, en un único script de arranque:

-- ServerScriptService/Boot.server.lua
local Praxsuite = require(game.ServerScriptService.PraxsuiteSDK)

Praxsuite.Init({
    workspaceId = "your-workspace-uuid",
    apiKey = "sk_live_...",                      -- solo para probar en Studio
    baseUrl = "https://gateway.praxsuite.com",
})

print("Praxsuite ready:", Praxsuite.IsInitialized())

Corre el place. La ventana Output imprime Praxsuite ready: true. Todavía nada salió de tu máquina - Init solo guarda configuración.

`baseUrl` es obligatorio, no opcional. Praxsuite corre en varios tiers independientes y tu workspace vive exactamente en uno. El host equivocado devuelve 404 en cada llamada, sin nada en el error que explique por qué, así que el SDK se niega a arrancar en vez de dejarte perseguir eso.

`apiKey` es solo para Studio. Un juego publicado usa apiKeySecret, que lee el valor del Roblox Secrets Store:

Praxsuite.Init({
    workspaceId = "your-workspace-uuid",
    apiKeySecret = "PraxsuiteKey",               -- nombre en el Secrets Store
    baseUrl = "https://gateway.praxsuite.com",
})

Agrega el secreto en Game Settings → Security → Secrets Store, con el nombre PraxsuiteKey. HttpService:GetSecret() no funciona en Studio, solo en un juego publicado - y justamente por eso existe la opción de apiKey en crudo.

¿Por qué un módulo de configuración en vez de llamar a Init?

Hay una segunda forma, y para un proyecto real es la mejor. Crea un ModuleScript llamado exactamente PraxsuiteConfig en ServerScriptService:

-- ServerScriptService/PraxsuiteConfig  (ModuleScript)
return {
    workspaceId = "your-workspace-uuid",
    apiKeySecret = "PraxsuiteKey",
    baseUrl = "https://gateway.praxsuite.com",
}

Ahora ningún script llama a Init - el primer script que usa el SDK encuentra ese módulo y lo configura. La ventaja es el orden: un Init explícito necesita que tu script de arranque corra antes que todo lo demás, y Roblox no te da esa garantía. Con el módulo de configuración, el script que llegue primero dispara el setup.


Step 3 - Registrar a un Usuario

En Roblox, la mayoría de los juegos no necesitan su propia pantalla de login. Roblox ya autenticó al jugador, y player.UserId es una identidad verificada en la que tu servidor puede confiar. El trabajo del SDK es registrar esa identidad en tu workspace:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    Praxsuite.Players.Identify(player, {
        metadata = { accountAge = player.AccountAge },
    })
end)

Players.PlayerRemoving:Connect(function(player)
    Praxsuite.Players.Forget(player)
end)

Identify registra al jugador en segundo plano, así que nunca retrasa su aparición. Forget limpia la caché local. Lee el registro guardado con Praxsuite.Players.GetInfo(player).

¿Qué es un UserId? Cada cuenta de Roblox tiene un número único y permanente. Es la única forma confiable de identificar a un jugador: el nombre de usuario puede cambiar, el UserId no. Guárdalo como texto cuando la columna sea ShortText: `tostring(player.UserId)`. Importante: `Identify` es una etiqueta, no un permiso. Registra quién es el jugador; no limita ninguna query.

¿El SDK limita las queries por jugador?

Por defecto, no - y para la mayoría de los juegos en Roblox ese es el default correcto. El SDK corre en tu servidor de juego con una server key, lo que hace que tu servidor sea la parte de confianza - exactamente como en una escritura de DataStore. Sin ninguna opción extra, tú aplicas las reglas por jugador en tu propio código, como siempre.

La versión 1.0.0 eliminó una opción asPlayer que parecía hacer esto pero no hacía nada: solo fijaba dos headers de petición que ninguna parte del Gateway leía, una frontera de seguridad que no limitaba nada. Una actualización posterior del SDK trajo asPlayer de vuelta, esta vez conectada a algo real, junto con un nuevo módulo Auth. Praxsuite.Auth.LoginPlayer(player) abre una sesión por jugador a partir de player.UserId - sin pantalla de login, sin contraseña - y pasar { asPlayer = player } a una llamada de Data o Endpoints manda la sesión de ese jugador en vez de la server key, así que los filtros de fila propios de la tabla se aplican a él en vez de a todos:

Praxsuite.Auth.LoginPlayer(player)  -- por ejemplo en PlayerAdded

Praxsuite.Data.Query("partidas", {
    where = { completada = true },
}, { asPlayer = player })

Si encuentras el asPlayer viejo, el que solo ponía headers, en un ejemplo anterior a esto, bórralo - esos headers siguen sin ser leídos por nada. Este es distinto: lleva un token de sesión real, y primero necesita un proveedor roblox registrado y una key de servidor marcada para esa plataforma en el portal.

Auth.LoginPlayer te da una identidad de Praxsuite estable, propia de este juego de Roblox. Si en cambio necesitas una cuenta compartida - el mismo email y contraseña iniciando sesión también desde Unity o un navegador - eso es otra cosa que el SDK no te construye; llamas tú mismo a las rutas de auth del Gateway, como hace la Parte 3 de la guía Lua SDK Use Case en Roblox.


Step 4 - Leer y Escribir Datos

Cada método de aquí habla con una Table por su nombre. Escribe una fila:

local inserted = Praxsuite.Data.Insert("player_profiles", {
    roblox_id = tostring(player.UserId),
    coins = 100,
})

print("New row id:", inserted.Id)

Revisa la Table en el portal: la fila está ahí, con un Id que generó la base de datos. Léela de vuelta:

local rows = Praxsuite.Data.Query("player_profiles", {
    where = { roblox_id = tostring(player.UserId) },
    select = { "roblox_id", "coins" },
    orderBy = { "coins", "desc" },
    limit = 10,
})

print("Found", #rows, "profiles")

Query siempre devuelve un array, vacío si nada coincidió - nunca nil, así que #rows siempre es seguro.

Update y delete exigen un where. El SDK rechaza una escritura sin alcance antes de enviar nada:

Praxsuite.Data.Update("player_profiles", {
    set = { coins = 250 },
    where = { roblox_id = tostring(player.UserId) },
})

Deja afuera el where y obtienes Update requires 'where' clause (no unscoped updates) de inmediato, en vez de una table vacía.

La table where acepta los trece operadores que implementa el Gateway - eq neq gt gte lt lte like ilike in is between contains textsearch - escritos como { coins = { gt = 100 } }. Un valor suelto significa eq. Cuatro nombres más (isNull, isNotNull, startsWith, endsWith) no existen del lado del servidor y el SDK los traduce por ti.

No existe notIn. Pídelo y el SDK lanza un error diciéndote que uses un in positivo - porque el parser del Gateway lo rechaza, y fallar en tu editor es mejor que fallar en un juego en vivo.

Escribir muchas filas de una vez

Cada llamada al SDK es un viaje de ida y vuelta por HTTPS. Diez filas escritas en un loop son diez viajes; esas mismas diez filas con InsertMany son uno:

local rows = {}
for i = 1, 5 do
    table.insert(rows, {
        roblox_id = tostring(player.UserId),
        coins = math.random(10, 500),
    })
end

local inserted = Praxsuite.Data.InsertMany("player_profiles", rows)
print("Se insertaron", #inserted, "filas")

Usa Insert para una fila e InsertMany para más de una. En Roblox esto no es una microoptimización: cada servidor tiene un tope de 500 peticiones HTTP por minuto, y un loop de inserts individuales se come ese presupuesto rápido.

Cuando las operaciones son distintas entre sí, Data.Batch manda inserts, updates y deletes juntos en la misma petición.

Contar sin leer

Para saber cuántas filas cumplen una condición, no las traigas para contarlas en Lua: pregúntale al Gateway.

local total = Praxsuite.Data.Count("player_profiles")
local ricos = Praxsuite.Data.Count("player_profiles", { coins = { gt = 100 } })

La base cuenta del lado del servidor y devuelve un número, no las filas.

Paginar los resultados

limit limita cuántas filas vuelven; offset dice cuántas saltear. Juntos paginan:

-- página 1
Praxsuite.Data.Query("player_profiles", { orderBy = { "coins", "desc" }, limit = 25, offset = 0 })

-- página 2
Praxsuite.Data.Query("player_profiles", { orderBy = { "coins", "desc" }, limit = 25, offset = 25 })

Pon siempre un limit. Sin él, una tabla que creció más de lo que esperabas convierte una query rápida en una lenta, en silencio.

El `%` en `like`: significa "cualquier cosa". `{ nickname = { like = "%dragon%" } }` matchea cualquier nickname que contenga `dragon`; `"dragon%"` matchea los que empiezan con eso. Usa `ilike` cuando las mayúsculas no deban importar. Los nombres de columna son exactos. `coins` y `Coins` son dos columnas distintas, y un nombre escrito con espacio no es el mismo nombre escrito con guion bajo. Un nombre mal escrito es la causa más común de que una escritura parezca funcionar y el valor vuelva como `nil`. Copia los nombres desde el portal en vez de reescribirlos.

Si no encuentra el nombre de una tabla

Por defecto el SDK trae el registro de tablas del workspace en su primera llamada, que es lo que te permite referirte a las tablas por nombre. Si tu key no puede ver una tabla, o prefieres evitar esa consulta al arrancar, registra el mapeo tú mismo:

Praxsuite.Schema.Register("player_profiles", "2785e1d3-4a78-4d1a-a30c-7071c268e718")

El UUID está en el portal, en Gateway → Playground: selecciona la tabla en el riel lateral y copia el identificador que aparece.

prax-roblox-3.png

Step 5 - Llamar a un Gateway Endpoint

Una Table es datos; un Endpoint es lógica. Cuando un jugador compra algo, la decisión de si puede pagarlo no debe vivir en tu juego, porque tu juego corre en una máquina que el jugador controla.

Crea un endpoint Sync en el portal, enlázalo a una Automation y llámalo:

local result = Praxsuite.Endpoints.Call("validate-purchase", {
    player_id = player.UserId,
    product_id = "sword_of_fire",
})

if result.approved then
    grantItem(player, "sword_of_fire")
end

Call se bloquea hasta que la Automation termina y devuelve lo que produjo su nodo Response, ya parseado. El cliente del jugador nunca ve el precio, el saldo ni la regla.

Cuando no necesitas una respuesta, usa Fire - devuelve true si el Gateway aceptó la petición y no espera:

Praxsuite.Endpoints.Fire("on-player-leave", {
    player_id = player.UserId,
    play_duration = os.time() - joinTime,
})

Analytics y logging van en Fire. Todo lo que la línea siguiente dependa va en Call.

imagen_2026-08-28_092622915.png

Ejemplo Completo

Todo lo anterior, en un solo script de servidor que corre tal cual está escrito:

-- ServerScriptService/GameBackend.server.lua
local Players = game:GetService("Players")
local Praxsuite = require(game.ServerScriptService.PraxsuiteSDK)

Praxsuite.Init({
    workspaceId = "your-workspace-uuid",
    apiKey = "sk_live_...",                      -- solo Studio
    baseUrl = "https://gateway.praxsuite.com",
})

local function loadProfile(player)
    local rows = Praxsuite.Data.Query("player_profiles", {
        where = { roblox_id = tostring(player.UserId) },
        limit = 1,
    })

    if #rows > 0 then
        return rows[1]
    end

    return Praxsuite.Data.Insert("player_profiles", {
        roblox_id = tostring(player.UserId),
        coins = 100,
    })
end

Players.PlayerAdded:Connect(function(player)
    Praxsuite.Players.Identify(player)

    local ok, profile = pcall(loadProfile, player)
    if not ok then
        warn("[Game] Could not load profile:", profile)
        return
    end

    print(player.Name, "has", profile.coins, "coins")
end)

Players.PlayerRemoving:Connect(function(player)
    Praxsuite.Endpoints.Fire("on-player-leave", {
        player_id = player.UserId,
    })
    Praxsuite.Players.Forget(player)
end)

Presiona Play. El Output imprime el saldo de monedas del jugador y aparece una fila en tu Table. Ese ida y vuelta - de Studio al Gateway, a la base de datos y de regreso - es toda la integración.


Errores Comunes y Cómo Evitarlos

Error

Causa

Solución

[PraxsuiteSDK] baseUrl is required. Copy it from your workspace's API Gateway settings page

Se llamó a Init sin baseUrl. No tiene default a propósito.

Agrega baseUrl = "https://gateway.praxsuite.com", o el host de tu tier.

[PraxsuiteSDK] Not initialized. Either: 1. Call Praxsuite.Init(...)

Un script usó el SDK antes de que algo lo configurara, y no se encontró el módulo PraxsuiteConfig.

Agrega el ModuleScript PraxsuiteConfig a ServerScriptService, o llama a Init antes.

[PraxsuiteSDK] Table 'x' not found in registry

El nombre de la table no existe en el workspace, o el fetch de schema falló porque la key no puede verla.

Revisa la ortografía, revisa los alcances de la key, o regístrala manualmente con Praxsuite.Schema.Register("x", "uuid").

[PraxsuiteSDK] HTTP_401: Unauthorized

Key equivocada, key revocada, o la key no tiene alcance sobre esa table.

Vuelve a copiar la key desde Gateway → Credentials y confirma sus alcances.

[PraxsuiteSDK] Update requires 'where' clause (no unscoped updates)

Un Update o Delete sin where. El SDK lo rechaza antes de enviar.

Agrega un where que identifique las filas. Esta validación existe para que no borres una table por accidente.

[PraxsuiteSDK] There is no 'notIn' operator - the gateway does not implement one

Un where usó notIn.

Exprésalo como un in positivo sobre los valores que sí quieres, o filtra en Lua después de la query.

La escritura funciona pero el valor vuelve como nil, o la columna queda vacía en el portal

Una clave de tu table de Lua no coincide exactamente con el nombre de la columna: mayúsculas distintas, un espacio donde va un guion bajo.

Copia los nombres de columna desde el portal. Distinguen mayúsculas y espacios.

No pasa nada y la ventana de Output queda en silencio

El código está en un LocalScript, que nunca corre en el servidor.

Muévelo a un Script dentro de ServerScriptService. Un LocalScript además filtraría tu key.

Http requests are not enabled

Roblox mismo bloqueó la llamada.

Game Settings → Security → Allow HTTP Requests.

Consejo: Todo error del SDK es un string que empieza con `[PraxsuiteSDK]`. Envuelve las llamadas en `pcall` y el segundo valor de retorno es ese string - imprímelo, no te lo tragues.


Consejos de Producción

  • Mueve la key al Secrets Store antes de publicar. Cambia apiKey por apiKeySecret.

  • Limita la key a las tables que el juego realmente usa. Una key filtrada que lee una table es un incidente; una que escribe todo es un desastre.

  • Respeta el límite de 500 peticiones por minuto por servidor. Es un límite de Roblox, no nuestro. Agrupa escrituras relacionadas con Data.Batch.

  • Pon todo lo valioso detrás de un Endpoint. Monedas, inventario y puntajes escritos directo desde el juego son tan confiables como el servidor del juego.

  • Envuelve cada llamada en `pcall`. El SDK lanza errores en caso de falla, y un tropiezo sin manejar en un handler de PlayerAdded rompe la entrada de ese jugador.


Próximos Pasos

  • Mueve la validación de compras a una Automation y llámala con Endpoints.Call.

  • Agrega un leaderboard con Data.Query, orderBy y limit, refrescado con un timer en vez de por petición.

  • Junta las escrituras de fin de ronda de un jugador en una sola llamada a Data.Batch.

  • Lee la guía Lua SDK Use Case en Roblox, donde todo esto se convierte en un juego completo con autoridad del lado del servidor.

Ahora tienes un juego que habla con un backend real. Todo lo que sigue es decidir qué va de cada lado de esa línea.