rbxsync 0.1.1

Declaratively manage Roblox game passes, badges, and developer products
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# rbxsync

Declaratively manage Roblox game passes, badges, and developer products from a single TOML config file.

rbxsync syncs your local configuration to Roblox, tracks remote state in a lockfile, detects icon changes with [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) hashing, and generates a Luau module with all your asset IDs.

## Features

- **Declarative config** - Define all your passes, badges, and products in a single `rbxsync.toml`
- **Two-way sync** - Push local changes to Roblox or pull remote state to your config and lockfile
- **Icon management** - Upload icons, detect changes via BLAKE3 hashing, download remote icons
- **Conflict detection** - Detects when remote icons differ from local and lets you choose which to keep
- **Code generation** - Generates a Luau module (+ optional TypeScript definitions) mapping resource names to asset IDs
- **Flat & nested styles** - Choose between flat path-like keys or nested tables
- **Custom codegen paths** - Remap sections and individual items to custom paths
- **Extra entries** - Inject asset IDs for manually managed assets or other universes into the generated file
- **Alpha bleed** - Applies alpha bleeding to icons before uploading (enabled by default)
- **Duplicate detection** - Warns when multiple remote resources share the same name

## Installation

### Rokit

```sh
rokit add dev-bap/rbxsync
```

### Cargo

```sh
cargo install rbxsync
```

## Quick Start

### From an existing experience

```sh
rbxsync init --from-remote --universe-id 123456 --api-key YOUR_API_KEY
```

This fetches all your existing passes, badges, and products, downloads their icons, and generates both the config and lockfile. You can find your universe ID in the Creator Hub under your experience's URL.

### From scratch

```sh
rbxsync init
```

This creates a `rbxsync.toml` template. Edit it with your universe ID, creator info, and resources, then run:

```sh
rbxsync sync --api-key YOUR_API_KEY
```

## Commands

<details>
<summary><code>rbxsync init</code></summary>

Initialize a new config file.

| Flag | Description |
| --- | --- |
| `--from-remote` | Populate config from existing remote resources |
| `--universe-id` | Universe ID (required with `--from-remote`) |

</details>

<details>
<summary><code>rbxsync sync</code></summary>

Sync local config to Roblox. Creates, updates, and tracks resources.

| Flag | Description |
| --- | --- |
| `--dry-run` | Show what would change without applying |
| `--only` | Only sync specific types: `passes`, `badges`, `products` (comma-separated) |
| `--badge-cost` | Expected cost in Robux when creating a badge (default: `0`) |

</details>

<details>
<summary><code>rbxsync pull</code></summary>

Pull remote state into the config and lockfile.

Remote is the source of truth: remote-visible fields (`name`, `price`, `description`, etc.) are updated in the config while config-only fields (`icon`, `path`, `regional_pricing`) are preserved. New remote resources are added to the config.

| Flag | Description |
| --- | --- |
| `--dry-run` | Show what remote state differs without writing anything |
| `--accept-remote` | Download remote icons and update local files |
| `--accept-local` | Keep local icons and re-upload on next sync |

</details>

<details>
<summary><code>rbxsync check</code></summary>

Validate config, check lockfile consistency, and report if anything is out of sync.

</details>

<details>
<summary><code>rbxsync rename &lt;resource&gt; &lt;old_key&gt; &lt;new_key&gt;</code></summary>

Rename a resource key in both config and lockfile. The display name is preserved automatically.

```sh
rbxsync rename passes VIP vip_pass
```

</details>

<details>
<summary><code>rbxsync list &lt;resource&gt;</code></summary>

List remote resources. `resource` is one of: `passes`, `badges`, `products`.

</details>

## Configuration

rbxsync requires a `rbxsync.toml` file in the working directory (or specify with `--config`).

```toml
[experience]
universe_id = 123456789

[experience.creator]
type = "group"         # "user" or "group"
id = 123456

[codegen]
output = "src/shared/GameIds.luau"
# typescript = false   # Also generate a .d.ts file
# style = "flat"       # "flat" (default) or "nested"

[icons]
bleed = true           # Apply alpha bleed before uploading (default: true)
dir = "icons"          # Directory for downloaded icons (default: "icons")

[passes.VIP]
name = "VIP Pass"      # explicit display name on Roblox
price = 499
description = "VIP access to exclusive areas"
icon = "icons/vip.png"

[passes.Premium]       # name defaults to "Premium"
price = 999
description = "Premium membership"
icon = "icons/premium.png"

[badges.Welcome]       # name defaults to "Welcome"
description = "Welcome to the game!"
icon = "icons/welcome.png"
enabled = true

[products.Coins100]    # name defaults to "Coins100"
price = 99
description = "100 coins"
icon = "icons/coins.png"
```

<details>
<summary><code>[experience]</code></summary>

| Field | Type | Description |
| --- | --- | --- |
| `universe_id` | `u64` | Your Roblox universe ID |
| `creator.type` | `string` | `"user"` or `"group"` |
| `creator.id` | `u64` | Your Roblox user or group ID |

</details>

<details>
<summary><code>[codegen]</code></summary>

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `output` | `string` | -- | Path to generate the Luau module (omit to disable) |
| `typescript` | `bool` | `false` | Also generate a TypeScript definition file (`.d.ts`) |
| `style` | `string` | `"flat"` | `"flat"` or `"nested"` (see [Code Generation]#code-generation) |

</details>

<details>
<summary><code>[codegen.paths]</code></summary>

Override the default section name for each resource type. Dot-separated segments become either a prefix (flat) or nested tables (nested).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `passes` | `string` | `"passes"` | Path for game passes |
| `badges` | `string` | `"badges"` | Path for badges |
| `products` | `string` | `"products"` | Path for developer products |

```toml
[codegen.paths]
passes = "gamepasses"
badges = "achievements"
products = "devproducts"
```

</details>

<details>
<summary><code>[codegen.extra]</code></summary>

Inject asset IDs into the generated file. Useful for manually managed assets or assets from other universes that you still want available in code.

```toml
[codegen.extra]
"passes.legacy_vip" = 1234567
"products.starter_pack" = 9876543
```

</details>

<details>
<summary><code>[icons]</code></summary>

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `bleed` | `bool` | `true` | Apply alpha bleed to images before uploading. Changing this won't invalidate the lockfile or reupload existing images |
| `dir` | `string` | `"icons"` | Directory for icons downloaded by `pull --accept-remote` |

</details>

<details>
<summary><code>[passes.&lt;name&gt;]</code></summary>

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | No | Display name (defaults to the TOML key) |
| `price` | `u64` | No | Price in Robux (omit for free) |
| `description` | `string` | No | Pass description |
| `icon` | `string` | No | Path to icon file |
| `for_sale` | `bool` | No | Whether the pass is for sale (default: `true`) |
| `regional_pricing` | `bool` | No | Enable regional pricing (default: `false`) |
| `path` | `string` | No | Override the codegen path for this item |

</details>

<details>
<summary><code>[badges.&lt;name&gt;]</code></summary>

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | No | Display name (defaults to the TOML key) |
| `description` | `string` | No | Badge description |
| `icon` | `string` | No | Path to icon file |
| `enabled` | `bool` | No | Whether the badge is active (default: `true`) |
| `path` | `string` | No | Override the codegen path for this item |

</details>

<details>
<summary><code>[products.&lt;name&gt;]</code></summary>

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | No | Display name (defaults to the TOML key) |
| `price` | `u64` | **Yes** | Price in Robux |
| `description` | `string` | No | Product description |
| `icon` | `string` | No | Path to icon file |
| `for_sale` | `bool` | No | Whether the product is for sale (default: `true`) |
| `regional_pricing` | `bool` | No | Enable regional pricing (default: `false`) |
| `store_page` | `bool` | No | Show on the store page (default: `false`) |
| `path` | `string` | No | Override the codegen path for this item |

</details>

## Authentication

rbxsync uses the [Roblox Open Cloud API](https://create.roblox.com/docs/cloud/open-cloud). Create an API key at https://create.roblox.com/dashboard/credentials.

You can provide the key via the `--api-key` flag or the `RBXSYNC_API_KEY` environment variable:

```sh
rbxsync sync --api-key YOUR_API_KEY
```

```sh
export RBXSYNC_API_KEY=YOUR_API_KEY
rbxsync sync
```

The `--api-key` flag takes precedence over the environment variable.

### Required API scopes

| Resource | Scopes | Documentation |
| --- | --- | --- |
| Game Passes | `game-pass:read`, `game-pass:write` | [Game Passes API]https://create.roblox.com/docs/cloud/api/game-passes |
| Developer Products | `developer-product:read`, `developer-product:write` | [Developer Products API]https://create.roblox.com/docs/cloud/api/developer-products |
| Badges | `legacy-universe.badge:read`, `legacy-universe.badge:write`, `legacy-universe.badge:manage-and-spend-robux` | [Badges API]https://create.roblox.com/docs/cloud/api/badges, [Universes - Badges]https://create.roblox.com/docs/cloud/features/universes#badges |
| Assets (icons) | `legacy-asset:manage` | [Assets]https://create.roblox.com/docs/cloud/features/assets#/ |

## Code Generation

When `codegen.output` is set, rbxsync generates a Luau module after every `sync`. The variable name is derived from the filename.

### Styles

#### Flat (default)

Items are stored with dot-separated path keys. This works well with TypeScript as the string-literal keys provide full autocomplete.

```toml
[codegen]
output = "src/shared/GameIds.luau"
style = "flat"
```

```lua
-- This file is auto-generated by rbxsync. Do not edit manually.

local GameIds = {
	["badges.Welcome"] = 98765,
	["passes.Premium"] = 67891,
	["passes.VIP"] = 67890,
	["products.Coins100"] = 11111,
}

return GameIds
```

Access: `GameIds["passes.VIP"]`

#### Nested

Items are organized into nested tables. Better for Luau since it provides direct table access without string keys.

```toml
[codegen]
output = "src/shared/GameIds.luau"
style = "nested"
```

```lua
-- This file is auto-generated by rbxsync. Do not edit manually.

local GameIds = {
	badges = {
		Welcome = 98765,
	},
	passes = {
		Premium = 67891,
		VIP = 67890,
	},
	products = {
		Coins100 = 11111,
	},
}

return GameIds
```

Access: `GameIds.passes.VIP`

### Custom paths

Use `[codegen.paths]` to remap entire sections, or per-item `path` to override individual items:

```toml
[codegen.paths]
passes = "gamepasses"
badges = "achievements"
products = "devproducts"

[products.special_offer]
price = 99
path = "shop.specials"       # overrides the section default
```

With `style = "nested"`, this produces:

```lua
local GameIds = {
	achievements = {
		Welcome = 98765,
	},
	gamepasses = {
		VIP = 67890,
	},
	devproducts = {
		Coins100 = 11111,
	},
	shop = {
		specials = {
			special_offer = 12345,
		},
	},
}
```

With `style = "flat"`:

```lua
local GameIds = {
	["achievements.Welcome"] = 98765,
	["gamepasses.VIP"] = 67890,
	["devproducts.Coins100"] = 11111,
	["shop.specials.special_offer"] = 12345,
}
```

### Extra entries

Inject asset IDs for manually managed assets or other universes into the generated file:

```toml
[codegen.extra]
"passes.legacy_vip" = 1234567
"products.starter_pack" = 9876543
```

These entries are merged alongside synced resources in the output. With `style = "flat"`:

```lua
local GameIds = {
	["passes.VIP"] = 67890,
	["passes.legacy_vip"] = 1234567,
	["products.Coins100"] = 11111,
	["products.starter_pack"] = 9876543,
}
```

### TypeScript

When `typescript = true`, a `.d.ts` file is generated alongside the Luau module:

```typescript
// This file is auto-generated by rbxsync. Do not edit manually.

declare const GameIds: {
	"badges.Welcome": number
	"passes.VIP": number
	"products.Coins100": number
}

export = GameIds
```

### Key escaping

Resource names that aren't valid Luau identifiers are automatically escaped:

```lua
["my-pass"] = 12345,
```

## Lockfile

rbxsync generates a `rbxsync.lock.toml` that tracks remote state: asset IDs, icon hashes, and metadata. Commit this file to version control.

## Icon Conflict Resolution

When you run `pull` and a remote icon differs from what's in the lockfile:

```
! pass 'VIP': icon differs from remote
  Local:  icons/vip.png (blake3: a1b2c3d4e5f6...)
  Remote: asset 129268487446043
```

Resolve with:
- `--accept-remote` -- Downloads the remote icon to your local path
- `--accept-local` -- Keeps your local icon and re-uploads it on next `sync`

## Attributions

Thank you to [Tarmac](https://github.com/Roblox/tarmac) for the alpha bleeding implementation, which was used in this project.