Roblox Open Cloud Asset Sync
Upload images, sounds, meshes, animations, and videos to Roblox through the Open Cloud Assets API — and get typed Luau or roblox-ts bindings back, automatically.
Quick start · CLI · Configuration · Studio plugin · Generated output · API reference
English | 日本語
Drop files into assets/, run rocas sync, and require them with full type safety:
local images = require(ReplicatedStorage.Shared.images)
imageLabel.Image = images.ui.button --> "rbxassetid://12345678"No manual asset ID copy-pasting, no stale IDs, no untyped string tables.
- All asset types — images, sounds, meshes, animations, videos
- Hash-based change detection — uploads only what actually changed, tracked in lock files
- Config-aware re-sync — re-uploads when the
rocas.tomlcreator orassetTypechanges, even if the file is byte-for-byte identical - Recursive directory scanning — nested folders become nested generated objects
- Luau native by default — generates
--!stricttype-annotated.luauoutput - roblox-ts compatible — opt in to
.luau+.d.tspairs in an Asphalt-like shape - Studio plugin — browse, search, preview, and insert synced assets without leaving Studio
- Local preview mode — browse local assets in Studio without an Open Cloud API key
- CLI + library — use
rocas syncorrequire("rocas")
| Node.js | 18 or newer |
| Roblox Open Cloud API key | Required for sync and watch only. Create one in the Creator Dashboard with Assets read/write permissions for your user or group. The read permission is what lets rocas resolve image IDs. |
Note
rocas plugin and rocas manifest --local work entirely offline — no API key needed.
Globally, as a CLI:
npm install -g github:Plumvery/rocasOr as a project dev dependency:
npm install --save-dev github:Plumvery/rocas1. Create rocas.toml in your project root
[creator]
type = "user"
id = 123456789
[[sync]]
name = "images"
path = "assets/images"
output = "src/shared/images"
[[sync]]
name = "sounds"
path = "assets/sounds"
output = "src/shared/sounds"2. Add your API key to .env
ROCAS_API_KEY="your-open-cloud-api-key"Tip
Make sure .env is listed in your .gitignore. Never commit an Open Cloud API key.
3. Sync
rocas syncAssets upload, and src/shared/images.luau + src/shared/sounds.luau are generated with the resulting asset IDs.
4. Keep it running while you work
rocas watch| Command | Description |
|---|---|
rocas sync |
Upload changed assets and regenerate bindings |
rocas watch |
Watch asset directories and rocas.toml, syncing on change |
rocas plugin |
Generate the static Roblox Studio plugin |
rocas manifest |
Generate a ReplicatedStorage manifest ModuleScript from lock files |
rocas help |
Show help |
| Flag | Applies to | Default | Description |
|---|---|---|---|
--debounce <ms> |
watch |
10000 |
Debounce interval before a sync fires |
--output, -o <path> |
plugin |
Studio local Plugins folder | Where to write the plugin |
--output, -o <path> |
manifest |
src/shared/RocasManifest.luau |
Where to write the manifest module |
--local |
manifest |
— | Build from local files instead of lock files; no upload, no API key |
| Variable | Description |
|---|---|
ROCAS_API_KEY |
Roblox Open Cloud API key. Set it in .env or export it in your shell. |
| Type | Extensions | Roblox assetType |
|---|---|---|
| Image | .png .jpg .jpeg .bmp .tga |
Decal |
| Audio | .mp3 .ogg .wav .flac |
Audio |
| Mesh | .fbx .glb .gltf .obj |
Model |
| Animation | .rbxm .rbxmx |
Animation |
| Video | .mp4 .mov |
Video |
Asset types are detected from the file extension, and can be overridden per group with assetType.
Open Cloud uploads images as Decal assets, and the ID it returns is the decal, not the image inside it. A decal ID works for Decal.Texture, but ImageLabel.Image, ImageButton.Image, ParticleEmitter.Texture, and friends want the image ID.
So after uploading an image, rocas downloads the decal and reads the image ID out of it, storing it as imageId in the lock file. Generated code, asset maps, and the Studio manifest all prefer imageId and fall back to the decal ID when it isn't there.
{
"ui/button.png": { "assetId": "12345679", "imageId": "12345678", "hash": "…", "config": "…" }
}This needs an API key with read access to Assets — Roblox has required authentication on asset delivery since April 2025. Images already in an older lock file are backfilled on the next rocas sync without re-uploading.
Note
Resolution never fails a sync. If the image ID can't be read (moderation still pending, key missing the read permission), rocas warns, keeps the decal ID, and retries on the next sync. After three failures in a row it stops trying for the rest of that group. Set resolveImageIds = false on a group to turn it off entirely.
[creator]
type = "user" # "user" or "group"
id = 123456789 # Roblox User ID or Group ID
[[sync]]
name = "images" # Group name, used for images.lock.json
path = "assets/images" # Directory to scan recursively
output = "src/shared/images" # Generates images.luau by default
# assetType = "Decal" # Optional: force asset type
# format = "luau" # "luau" (default) or "roblox-ts"
# stripExtensions = false # Remove file extensions from generated keys
# resolveImageIds = true # Resolve decal IDs to image IDs (default true)See rocas.toml.example for a fully commented reference.
| Value | Output | Description |
|---|---|---|
"luau" (default) |
.luau |
--!strict output with type annotations |
"roblox-ts" |
.luau + .d.ts |
roblox-ts / Asphalt-compatible output |
When true, file extensions are removed from generated keys:
-- stripExtensions = false (default)
images.ui["button.png"]
-- stripExtensions = true
images.ui.buttonrocas keeps a <name>.lock.json inside each synced directory (for example assets/images/images.lock.json). An asset is skipped only when both of these match the lock:
- the file content hash, and
- a fingerprint of the upload-affecting
rocas.tomlconfig (the[creator]type/idand the resolvedassetType).
Important
If you point rocas.toml at a different creator — say you change [creator].id from a group to your user — the next rocas sync re-uploads every affected asset under the new creator, even though the files themselves are unchanged.
Editing output, format, or stripExtensions only regenerates code; it never forces a re-upload. rocas watch also watches rocas.toml itself, so saving a config change reloads it and triggers a sync.
Upgrading from a pre-fingerprint version
Lock entries written by older versions of rocas have no config fingerprint. The first sync after upgrading records the current config as the baseline without re-uploading, so an upgrade alone never churns asset IDs.
If you need to force a full re-upload — for example, you changed the creator while still on a pre-fingerprint lock — delete the relevant *.lock.json and run rocas sync.
Generate the plugin once:
rocas pluginThe plugin is static — you never need to regenerate it when assets change.
Note
By default rocas writes the generated .rbxm directly into your Roblox Studio local Plugins folder, because Studio does not recognize .luau files there as local plugins. Use --output <path> to write it elsewhere; .lua and .rbxmx output paths are also supported.
After running rocas sync, generate a manifest ModuleScript for Rojo or Argon to sync into ReplicatedStorage:
rocas manifest --output src/shared/RocasManifest.luauThe plugin scans ReplicatedStorage for the manifest module, then opens an asset browser where you can search synced assets, preview images, inspect asset IDs, and click Insert to place references into the current place. When Rojo or Argon syncs a changed manifest into Studio, the plugin reloads the catalog automatically.
It also watches Script, LocalScript, and ModuleScript source changes inside Studio, updating each asset's usage count as matching asset IDs, paths, or file names appear in script source.
rocas manifest --local --output src/shared/RocasManifest.luauLocal manifests scan the asset folders in rocas.toml directly and do not require ROCAS_API_KEY. In Studio, click Import to choose matching files, or Load on a single row.
Warning
Local mode uses File:GetTemporaryId() and rbxtemp:// IDs. These references only work in the current Studio session — they are not shared, and not saved as permanent Roblox assets. Use rocas sync + rocas manifest when you need durable rbxassetid:// IDs for team, shared, or runtime use.
What Insert creates for each asset type
| Asset type | Insert target |
|---|---|
| Decal / Image | Selected BasePart as a Decal, or StarterGui as an ImageLabel when no part is selected |
| Audio | SoundService as a Sound |
| Model / Mesh | Workspace through InsertService:LoadAsset |
| Animation | ReplicatedStorage/rocas Animations as an Animation |
| Video | StarterGui as a VideoFrame |
Given this directory structure:
assets/images/
ui/
button.png
icon.png
fx/
spark.png
Luau format — default, shown with stripExtensions = true
images.luau:
--!strict
-- This file is auto-generated by rocas. Do not edit manually.
type ImagesType = {
fx: {
spark: string,
},
ui: {
button: string,
icon: string,
},
}
local images: ImagesType = {
fx = {
spark = "rbxassetid://12345678",
},
ui = {
button = "rbxassetid://23456789",
icon = "rbxassetid://34567890",
},
}
return imagesUsage:
local images = require(path.to.images)
imageLabel.Image = images.ui.buttonroblox-ts format — format = "roblox-ts"
images.luau:
-- This file is auto-generated by rocas. Do not edit manually.
local images = {
fx = {
["spark.png"] = "rbxassetid://12345678",
},
ui = {
["button.png"] = "rbxassetid://23456789",
["icon.png"] = "rbxassetid://34567890",
},
}
return imagesimages.d.ts:
// This file is auto-generated by rocas. Do not edit manually.
declare const images: {
fx: {
"spark.png": string
}
ui: {
"button.png": string
"icon.png": string
}
}
export = imagesUsage:
import images from "shared/images";
imageLabel.Image = images.ui["button.png"];const { loadConfig, loadEnv, syncAll } = require("rocas");
loadEnv();
const config = loadConfig();
await syncAll(config, process.env.ROCAS_API_KEY);Every export — sync, codegen, lock-file, and Studio plugin helpers — is documented in the API reference.
Registering a custom codegen format
Codegen formats are pluggable. A format is any object with a name and a render function that returns the files to write:
const { registerCodegenFormat, listCodegenFormats } = require("rocas");
registerCodegenFormat({
name: "json",
render(lock, varName, { stripExtensions = false } = {}) {
return [{ extension: ".json", content: JSON.stringify(lock, null, 2) }];
},
});
listCodegenFormats(); //=> ["luau", "roblox-ts", "json"]Each returned entry is { extension, content }, and is written next to the group's output path. Once registered, the format can be selected per sync group with format = "json" in rocas.toml.
git clone https://github.com/Plumvery/rocas.git
cd rocas
npm install
npm testmacOS: npm install fails with 'string.h' file not found
The rbxm-parser dependency pulls in lz4, a native module built with node-gyp. On macOS with Xcode selected as the active developer directory, node-gyp may fail to resolve the SDK headers:
../lib/binding/lz4_binding.cc:1:10: fatal error: 'string.h' file not found
Point it at the SDK explicitly:
export SDKROOT="$(xcrun --show-sdk-path)"
npm installAdd that export to your shell profile to make it stick.
Project layout
bin/
rocas.js CLI entry point
src/
index.js Public library surface
config.js .env + rocas.toml loading
sync.js Sync orchestration, extension → assetType mapping
upload.js Open Cloud Assets API client
asset-map.js Lock file reading and asset map building
codegen.js Code generation
formats/ Pluggable output formats (luau, roblox-ts)
studio-plugin.js Studio plugin and manifest generation
watch.js File watching
test/
test.js Test suite (node test/test.js)
Issues and pull requests are welcome — see CONTRIBUTING.md for the development workflow. Release history lives in CHANGELOG.md.
MIT © Plumvery