---
name: roblox-right-to-be-forgotten
description: Use when handling Right to be Forgotten (right to erasure, GDPR/CCPA deletion) requests for a Roblox game - the daily "Right to be Forgotten - Action Requested" messages, RTBF deletion templates in the Data Stores Manager, the Right to Erasure webhook, deleting data store, ordered data store and MemoryStore entries in Luau or Open Cloud - or when designing data store keys so per-player data is easy to delete.
---

# Right to be Forgotten requests

When a user asks Roblox to delete their account and personal data, Roblox tells every creator whose games they
played. The creator must then delete what they stored about that user. This skill covers what Roblox expects
(**policy**), and the tools that do the deleting (**API**).

> This is not legal advice. Roblox's own GDPR/CCPA page says the same about its guidance. If you are unsure whether you
> may keep some data, ask a lawyer.

## Policy (what Roblox and the law expect)

From Roblox's "About GDPR and CCPA" and "RTBF and creators" pages:

- **Act only on requests from Roblox.** They arrive as a daily Roblox message listing the user IDs and affected games,
  and through the Right to Erasure webhook if you set one up. If a player contacts you directly, send them to
  `https://www.roblox.com/support`.
- **User IDs count as personal data.** GDPR and CCPA define personal information broadly, including user IDs.
- **Delete it once there's no valid legal basis to keep it.** Default to deleting everything tied to the user. Only
  keep something if you have a real reason (a lawyer can tell you whether it counts).
- **Don't collect more than Roblox gives you** (user ID and username). No birthdays, real names, photos or contact
  details. If you already stored such data, remove it and stop collecting it.
- **You stay responsible.** Roblox keeps sending the daily message even if you automate deletion. Anything your
  templates or automation don't cover, you delete by hand.
- **Confirm within 30 days.** Roblox's RTBF page says to check that the data is gone within 30 days of the request
  showing up in your messages. Follow any deadline stated in the message itself.
- **Group games:** only the group owner receives these requests and can configure webhooks.

## What to delete

Search for the user ID everywhere you store data, not just the main save:

- Every **standard data store** key with the user ID in its name, key or scope: profile, inventory, settings,
  redeemed codes, daily rewards, purchase history. Include stores from older versions of the game.
- **Ordered data stores** (leaderboards), which need their own entries removed.
- The user's ID **inside other keys**: guild or party member lists, friend lists, trade or gift logs, reports,
  mailbox messages. Templates can't find these; your code must load the value, remove the user, and save it.
- **MemoryStore** sorted maps, hash maps and queues. Remove keyed items and keep expirations short (the maximum is 45
  days).
- **Anything outside Roblox**: your database, analytics, error trackers, Discord or Slack logs, spreadsheets, exported
  backups.

## Design keys so deletion is easy (API)

- One key per player per store, named with a fixed prefix and the user ID: `Player_{UserId}`. Never key by username.
- Keep per-user data in per-user keys. A shared key holding many players' data needs custom deletion code.
- Pass the user ID when writing (`store:SetAsync(key, value, { userId })`, or ProfileStore's `profile:AddUserId`) so the
  key's metadata names its owner.
- If you change a key format or store name, migrate the old data. Old stores still hold data and must still be deleted.

**ProfileStore** saves each profile in a standard data store named after the store, with your profile key as the key,
in the default `global` scope. **DataServiceTyped** uses its `profileStoreIndex` as the store name and
`"PLAYER_" .. UserId` as the key (the prefix changes if you set `profileStoreDataPrefix`).

## Option 1: RTBF deletion templates (automatic)

Roblox can delete matching data stores and keys for you whenever it processes a request. Set this up first.

Creator Dashboard > your game > **Configurations** > **Data stores manager** > **RTBF Deletion** tab > **Create Template**.
Pick a type and fill in a pattern containing `{UserId}`:

| Type | Example | Notes |
|---|---|---|
| Standard Key | store `PlayerData`, key `Player_{UserId}`, scope `global` | a ProfileStore profile |
| Ordered Key | store `CoinsLeaderboard`, key `Player_{UserId}`, scope `global` | a leaderboard entry |
| Standard Data Store | `Player_{UserId}_Save` | a whole per-user data store (standard only) |

- The token is case-sensitive: `{UserId}`, not `{userId}`.
- The user ID must be part of the data store name, key or scope, matched by a static pattern.
- A blank scope means `global`. You can have up to 100 templates.
- For group games, editing needs the Edit and publish experience permission (View Analytics to view).
- Templates can also be published through the Open Cloud Configs API as a `user_data_templates` config in the
  `DataStoresConfig` repository (API key with `universe:read` and `universe:write`).
- Test it: on a test game with a dummy account, write data for that account's ID, request RTBF for it, and confirm the
  data disappears.

## Option 2: delete it yourself

**In Luau** (a one-off admin script, or Studio with API access on the published place):

```luau
local DataStoreService = game:GetService("DataStoreService")

local function forgetUser(userId: number)
	local key = `Player_{userId}`
	DataStoreService:GetDataStore("PlayerData"):RemoveAsync(key)
	DataStoreService:GetOrderedDataStore("CoinsLeaderboard"):RemoveAsync(key)
end
```

- `GlobalDataStore:RemoveAsync(key)` writes a deletion marker. `GetAsync` then returns nil, older versions stay
  readable until they expire, and removed data is permanently deleted after 30 days.
- On an `OrderedDataStore`, `RemoveAsync` deletes the entry permanently (ordered stores keep no versions).
- ProfileStore: `PlayerStore:RemoveAsync(profileKey)` returns true on success. Only use it on a profile that has no
  active session and won't be loaded again.
- `DataStore:ListKeysAsync(prefix)` lists keys by prefix to check what's left. A prefix like `Player_12` also matches
  `Player_123`.
- MemoryStore: `MemoryStoreSortedMap:RemoveAsync(key)` and `MemoryStoreHashMap:RemoveAsync(key)`.

**From outside the game** (Open Cloud v2, header `x-api-key`):

| Delete | Endpoint | API key permission |
|---|---|---|
| Standard entry | `DELETE https://apis.roblox.com/cloud/v2/universes/{universe}/data-stores/{store}/entries/{key}` | `universe-datastores.objects:delete` |
| Standard entry in a scope | `.../data-stores/{store}/scopes/{scope}/entries/{key}` | same |
| Ordered entry | `.../universes/{universe}/ordered-data-stores/{store}/scopes/{scope}/entries/{key}` | `universe.ordered-data-store.scope.entry:write` |
| MemoryStore sorted map item | `.../universes/{universe}/memory-store/sorted-maps/{map}/items/{item}` | `memory-store.sorted-map:write` |

A standard entry deleted this way is marked `DELETED` and permanently removed after 30 days. 404 means the entry
doesn't exist; count that as done. To find keys, `GET .../data-stores/{store}/entries` takes
`filter=id.startsWith("Player_")`.

## Option 3: the Right to Erasure webhook

Use this for data that templates can't reach (IDs inside shared keys, external databases).

Creator Dashboard > your game > **Configurations** > **Webhooks** > **Add Webhook**: your HTTPS URL, a **Secret** (always set
one, or anyone could trigger deletions), trigger **Right to Erasure Request**.

```json
{
  "NotificationId": "string",
  "EventType": "RightToErasureRequest",
  "EventTime": "2023-12-30T16:24:24.2118874Z",
  "EventPayload": { "UserId": 1, "GameIds": [1234, 2345] }
}
```

`GameIds` are **start place IDs**. Open Cloud needs **universe IDs**, so keep a map from one to the other. The
`roblox-signature` header is `t=<timestamp>,v1=<signature>`. The signature is Base64 HMAC-SHA256, keyed with your
secret, of `<timestamp>.<JSON body>`.

```js
// Node 18+, Express. Follows the verification in Roblox's webhook docs.
const crypto = require("crypto");
const express = require("express");

const SECRET = process.env.ROBLOX_WEBHOOK_SECRET;
const app = express();
app.use(express.json());

app.post("/roblox", (req, res) => {
  const parts = String(req.headers["roblox-signature"] || "").split(",");
  const t = (parts.find((p) => p.startsWith("t=")) || "").substring(2);
  const v1 = (parts.find((p) => p.startsWith("v1=")) || "").substring(3);
  if (!t || !v1 || Date.now() - Number(t) * 1000 > 5 * 60 * 1000) return res.status(403).end();

  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${JSON.stringify(req.body)}`).digest("base64");
  const a = Buffer.from(v1), b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end();

  res.status(200).end(); // Roblox wants a 2XX within 5 seconds; do the work after
  if (req.body.EventType === "RightToErasureRequest") {
    queueErasure(req.body.NotificationId, req.body.EventPayload.UserId, req.body.EventPayload.GameIds);
  }
});
```

- `queueErasure` should store the job durably, run the Open Cloud deletes and your own database deletes, and retry
  failures. Roblox retries a failed delivery 5 times, but never after you've answered 200.
- Deduplicate by `NotificationId`; the same notification can arrive twice.
- **Keep a record without personal data**: `NotificationId`, date received, date completed, which stores and systems
  were cleared. Don't keep the user ID in long-lived logs or in the Discord channel the webhook posts to.

## Mistakes to avoid

- Deleting only the main profile and missing secondary stores, leaderboards, old renamed stores and shared keys.
- Forgetting copies: exported backups, analytics, error logs, Discord history, MemoryStore items with long expiries.
- A webhook endpoint without a secret or without signature and timestamp checks.
- Template typos: `{userId}`, a wrong scope (default is `global`), or a key pattern that doesn't match the live keys.
  Compare against real keys in the Data Stores Manager.
- Treating templates as the whole job. You still read the daily message and cover everything else.
- Acting on a player's DM or Discord message instead of a Roblox request.
- Storing personal data you don't need (real names, ages, emails, chat logs).
- Logging user IDs from requests "for the record". Record the `NotificationId` and what you deleted.
