# Platform Overview

How Qubiz is structured for players and creators: public games, creator uploads, player accounts, SDK permissions, comments, reports, and real source-backed data.

Tags: start, architecture, accounts, sdk

## What Qubiz provides

Qubiz is a browser-game portal for short-session games. Players can browse public games without an account, while account-only areas handle profile settings, creator uploads, cloud saves, comments, and reports.

The catalog is source-backed. Built-in demo games were removed. A game appears publicly only after a real published game record exists.

Uploaded games can use the Qubiz SDK for permissions, saves, leaderboards, and analytics events. Features that need an account or consent should keep the game playable when they are unavailable.

- Public: home, games catalog, status, stats, legal pages, docs, public profiles, published game pages.
- Account-only: profile, settings, developers, upload, creator dashboard.
- Creator-only: upload drafts, requested SDK scopes, review replies, and game analytics.
- Reports are submitted through the app flow, not direct public database writes.

## Recommended creator flow

A creator prepares an HTML5 game bundle, integrates the browser SDK, validates the metadata, uploads the ZIP and artwork, then waits for Qubiz review. Reviewers can publish, ask for changes, or send review messages. Creators reply from the creator dashboard.

Once published, the game page can show comments, reports, artwork, tags, requested SDK scopes, and later the playable runtime entry point.

- Build the game as a browser bundle.
- Use the SDK only for the permissions the game needs.
- Submit analytics events for progression, funnels, source attribution, and retention.
- Upload through Qubiz so the build, metadata, requested scopes, and creator attestations are reviewed together.
- Respond to review messages until the game is approved.

---

# JavaScript SDK

Install, configure, and call the browser SDK from uploaded games or local prototypes.

Tags: sdk, javascript, typescript, npm, browser

## Install options

The SDK can be used from npm in a bundled game, or directly from the browser bundle for plain HTML games. Use npm when your game has a build step; use the browser file for simple ZIP uploads.

#### npm install

```bash
npm install @qubiz/game-sdk
```

#### ES module import

```ts
import { QubizSDK } from "@qubiz/game-sdk";

QubizSDK.configure({
  targetOrigin: "https://qubiz.fun",
  timeoutMs: 8000
});
```

#### Browser script

```html
<script src="https://qubiz.fun/sdk/qubiz-game-sdk.js"></script>
<script>
  QubizSDK.configure({ targetOrigin: "https://qubiz.fun" });
</script>
```

## targetOrigin security

Always set targetOrigin in production. The SDK talks to the Qubiz host through postMessage. A specific origin prevents responses from being sent to an unexpected parent window.

Use the wildcard only for local experiments where the final host is unknown. Published games should use the production origin.

- Production: https://qubiz.fun
- Local testing: http://localhost:3000 or your current dev origin
- Avoid '*', except for very early prototypes

## getContext

getContext returns the game id, player identity fields allowed by the permission policy, and the scopes currently granted by the host.

Email and avatar are privacy-sensitive and may be null when the player has not granted those scopes.

#### Read player context

```ts
const context = await QubizSDK.getContext();

console.log(context.gameId);
console.log(context.player.id);
console.log(context.player.username);
console.log(context.grantedScopes);
```

## SDK surface map

The browser SDK is organized into small namespaces so game code can stay close to the game loop. Top-level methods cover context, permissions, saves, scores, and analytics; namespaces cover local events, assets, scripts, debug console, rooms, progress, economy, sandbox execution, and CLI command hints.

- QubizSDK.events - local event bus for game systems, editor events, and host events.
- QubizSDK.assets - list reviewed project assets and preload images, sprites, audio, models, scripts, data, fonts, and UI files.
- QubizSDK.scripting and QubizSDK.sandbox - request isolated script execution from the host.
- QubizSDK.debug - open a developer console overlay, set the hotkey, and forward logs.
- QubizSDK.multiplayer - create, join, leave, update, and send events to realtime rooms.
- QubizSDK.cloud - save, load, and remove player data.
- QubizSDK.achievements, QubizSDK.quests, QubizSDK.economy - player progression and economy APIs.
- QubizSDK.analytics - track events directly or create reusable analytics hooks.

---

# Create Studio

Use the Qubiz Create page to edit a project, preview browser code, inspect assets, and keep SDK console feedback close to the game loop.

Tags: create, editor, preview, developer-console

## Workspace model

Create is the developer workspace for uploaded projects and local prototypes. It loads existing Firebase game manifests for the signed-in creator, keeps a local code editor, runs a sandboxed preview iframe, and shows the asset shelf and declared SDK scopes next to the preview.

A project can start as a local prototype in Create, then move through Upload when the build ZIP, artwork, assets, scopes, and attestations are ready for review.

- Projects panel - switch between uploaded manifests or a new local prototype.
- Editor - work against main.js, sdk.ts, and manifest-style files.
- Preview - runs HTML/JS inside an iframe sandbox.
- Console - receives Create logs and can be paired with QubizSDK.debug.
- Asset shelf - shows starter assets and uploaded project assets.

## Preview with the SDK

The Create preview loads the same browser SDK bundle used by uploaded games. That lets developers exercise the event bus, debug overlay, asset preloading, analytics hooks, and host-bridge status handling before publishing.

#### Preview boot

```js
QubizSDK.debug.setHotkey({ key: "`", ctrlKey: true });
QubizSDK.events.emit("studio:preview", { scene: "main" });
await QubizSDK.debug.log("Preview booted");
```

---

# Event System

A local event bus for game systems, editor integrations, host events, and reusable gameplay hooks.

Tags: events, event-bus, hooks, game-loop

## Local event bus

Use QubizSDK.events for in-game events that should not always become analytics. The event bus is synchronous and local to the game frame. It also receives host events posted with source=nexa-host-event.

#### Subscribe and emit

```ts
const off = QubizSDK.events.on("quest:completed", (event) => {
  console.log(event.name, event.payload);
});

QubizSDK.events.emit("quest:completed", {
  questId: "tutorial-dash",
  reward: "dash-boots"
});

off();
```

#### Track only durable analytics

```ts
QubizSDK.events.on("quest:completed", async (event) => {
  await QubizSDK.analytics.track({
    name: "quest_completed",
    source: "quest-system",
    metadata: event.payload as Record<string, string | number | boolean>
  });
});
```

---

# Asset Pipeline

Upload reviewed source assets with a game and load them from the SDK asset library.

Tags: assets, marketplace, upload, preload

## Upload assets with a build

The Upload page supports a project asset pack in addition to the build ZIP, cover, and banner. Asset files are stored under game-uploads/{uid}/{slug}/asset and attached to the game manifest as assets[].

Assets can represent sprites, images, audio, models, scripts, data, fonts, UI files, and other reviewed files. The manifest keeps file metadata, tags, inferred kind, storage path, and download URL.

- Asset files: up to 30 files per upload draft.
- Per-file asset UI limit: 50 MB.
- Storage rule limit: 100 MB per uploaded object.
- The assets:read scope gates SDK access to the project library.

## List and preload assets

Use list to query reviewed assets and preload to warm the browser cache before a level, cutscene, or mode starts. Preload runs in the game frame; list is a host request.

#### Load sprites

```ts
await QubizSDK.requestPermissions(["assets:read"]);

const result = await QubizSDK.assets.list({
  kind: "sprite",
  tags: ["player"]
});

if (result.status === "granted") {
  await QubizSDK.assets.preload(result.assets);
}
```

---

# Scripting and Sandbox

Request isolated script execution for creator-authored code without granting broad page privileges.

Tags: scripting, sandbox, permissions, security

## Run scripts through the host

QubizSDK.scripting.run and QubizSDK.sandbox.run use the same host bridge. The game asks the host to execute a reviewed script by scriptId or source. The host decides whether sandbox execution is available for the project and returns granted, denied, prompt_required, or unavailable.

The SDK does not eval arbitrary strings in the game frame. That keeps runtime scripting behind declared permissions and future host-side sandbox controls.

#### Sandbox request

```ts
const permission = await QubizSDK.requestPermissions([
  "scripts:run",
  "sandbox:execute"
]);

if (permission.status === "granted") {
  const result = await QubizSDK.sandbox.run({
    scriptId: "enemy-wave-director",
    args: { wave: 4 },
    timeoutMs: 1000
  });
  console.log(result.status, result.output, result.logs);
}
```

---

# Debug Overlay and Console

Open a lightweight in-game developer console, configure the hotkey, and forward debug logs to the host.

Tags: debug, console, hotkey, overlay

## Configurable hotkey

The SDK debug overlay is local to the game frame. By default, Ctrl+` toggles it. Games can switch to a single key or a different modifier combination.

- debug.log, debug.info, debug.warn, and debug.error append lines to the overlay.
- The host bridge can also receive debugLog events when debug:console is granted.
- Use the overlay for creator diagnostics, not player-facing UI.

#### Open the console

```ts
QubizSDK.debug.setHotkey({ key: "F10" });
QubizSDK.debug.show();

await QubizSDK.debug.info("Spawner ready", {
  enemies: 12,
  biome: "factory"
});
```

---

# Permissions

Every SDK capability maps to a permission scope. Games should request only what they actually need.

Tags: permissions, privacy, consent, scopes

## Permission matrix

Qubiz evaluates SDK calls against the saved SDK permission policy. Some low-risk data, such as display name, can be allowed by default. Higher-risk data and writes can require player consent.

- profile:username - read the player's public display name.
- profile:avatar - read the player's public avatar URL.
- profile:email - read the player's email address; high-risk and should rarely be needed.
- assets:read - read reviewed project and marketplace assets.
- scripts:run - request creator script execution from the host.
- debug:console - open the SDK debug overlay and forward developer logs.
- storage:save - write local/cloud save data for the current game.
- storage:read - read game-specific cloud save data.
- leaderboard:submit - submit scores to public leaderboards.
- multiplayer:rooms - create, join, leave, and send realtime room events.
- achievements:write - unlock player achievements for the current game.
- quests:write - update quest objective and completion state.
- economy:read - read wallet balances and inventory summaries.
- economy:write - write currency, item, and economy transactions.
- payments:purchase - reserved for future purchase flows.
- analytics:events - send creator analytics events to Qubiz.
- sandbox:execute - execute isolated project code with bounded host capabilities.

## requestPermissions

Call requestPermissions before using features that need consent. The result tells you which scopes are granted, denied, or still require a prompt.

Do not block the whole game if a non-essential permission is denied. Good games degrade gracefully.

#### Ask for cloud saves and analytics

```ts
const result = await QubizSDK.requestPermissions([
  "storage:save",
  "analytics:events"
]);

if (result.status === "granted") {
  await QubizSDK.saveData("slot-1", { level: 4 });
}
```

---

# SDK API Reference

Complete method reference for the browser SDK: context, permissions, events, assets, scripts, debug, multiplayer, cloud saves, progress, economy, scores, and analytics.

Tags: api, methods, reference, postmessage

## Method summary

All SDK methods return promises. Host responses include status strings so games can show UI, retry, or continue without the feature.

- getContext(): returns gameId, player fields, and grantedScopes.
- requestPermissions(scopes): asks the host to evaluate permission scopes.
- saveData(key, value): saves bounded data for the current player and game.
- loadData(key): loads a bounded save payload for the current player and game.
- deleteData(key): removes a save key.
- submitScore(leaderboard, score): submits a numeric score to the current game leaderboard.
- trackEvent(event): sends a creator analytics event.
- events.on/once/off/emit: local game-frame event bus.
- assets.list(query): asks the host for reviewed project assets.
- assets.preload(assets): preloads asset URLs in the game frame.
- scripting.run(input) and sandbox.run(input): request isolated script execution.
- debug.setHotkey/show/hide/toggle/log/info/warn/error: developer console controls.
- multiplayer.createRoom/joinRoom/leaveRoom/sendEvent/updateState: realtime room APIs.
- cloud.save/load/remove: saveData/loadData/deleteData aliases.
- achievements.unlock(input): unlock a game-specific achievement.
- quests.update(input): write quest progress.
- economy.getWallet/transact: read balances and record currency/item transactions.
- analytics.track/hook: track analytics directly or create reusable event hooks.
- cli.command(action, args): produce Qubiz CLI command strings.

## Status values

SDK write methods return a status. Treat anything other than granted as a soft failure and keep the game playable.

- granted - the host accepted the operation.
- denied - the permission or payload was rejected.
- prompt_required - the user must be asked before the operation can continue.
- unavailable - Firebase, login, or the host bridge is unavailable.
- timeout - the host did not respond before timeoutMs.
- error - the host returned an unexpected failure.

## Host bridge message types

The browser bundle wraps postMessage for you, but advanced developers can still reason about the host bridge. The game sends source=nexa-game-sdk with an id, type, and params. The host replies with source=nexa-host, the same id, ok, result, and optional error.

- getContext - params are empty.
- requestPermissions - params.scopes is a list of permission strings.
- saveData - params.key is a short string and params.value is JSON-like data.
- loadData and deleteData - params.key is a short string.
- submitScore - params.leaderboard is a string and params.score is numeric.
- trackEvent - params contains name, stage, source, value, and metadata.
- listAssets - params can contain kind, tags, projectId, and includeMarketplace.
- runScript - params can contain scriptId, source, args, timeoutMs, and permissions.
- debugLog - params contain level, message, and optional data.
- createRoom, joinRoom, leaveRoom, sendRoomEvent, updateRoomState - room lifecycle and event payloads.
- unlockAchievement and updateQuest - progress ids and optional metadata.
- getWallet and economyTransaction - wallet reads and economy mutations.

---

# Save Data

Use saveData for player progress, settings, unlocks, and small game-state snapshots.

Tags: saveData, storage, progress, limits

## Basic save example

saveData is intended for compact state. Do not store huge maps, logs, images, or full replays. Keep the save key stable and the value JSON-serializable.

- Save keys are capped at 64 characters.
- Serialized save payloads are capped at 20,000 characters.
- Use one key per save slot or feature area.
- Avoid putting personal data in saveData.

#### Save a checkpoint

```ts
await QubizSDK.requestPermissions(["storage:save"]);

const saveResult = await QubizSDK.saveData("main-slot", {
  level: 8,
  coins: 420,
  unlocked: ["dash", "double-jump"],
  updatedAt: new Date().toISOString()
});

if (saveResult.status !== "granted") {
  console.warn("Save skipped", saveResult.status);
}
```

---

# Leaderboards

Submit public scores for the current game. Scores are entertainment features and should not be treated as cheat-proof competitive records.

Tags: leaderboard, score, submitScore

## submitScore

submitScore writes a numeric score for the current player and game. Round values before submitting, and submit only important scores rather than every frame.

- Submit final scores, best scores, or milestone scores.
- Do not submit every tick.
- Use your own anti-spam logic inside the game UI.
- The host may reject unavailable or invalid submissions.

#### Submit a score

```ts
const permission = await QubizSDK.requestPermissions(["leaderboard:submit"]);

if (permission.status === "granted") {
  const result = await QubizSDK.submitScore("global", Math.round(score));
  console.log(result.status, result.score);
}
```

---

# Multiplayer Toolkit

Create rooms, join sessions, leave cleanly, send realtime room events, and update shared room state.

Tags: multiplayer, rooms, realtime, firebase

## Room lifecycle

The multiplayer namespace wraps the Qubiz host bridge. Games request multiplayer:rooms, then create or join a room and exchange small event payloads. The current host uses Firebase Realtime Database when configured and returns unavailable when realtime services are absent.

- Room event payloads should stay small and JSON-like.
- Use authoritative server logic for competitive results when prizes or rankings matter.
- Leave a room when the player exits the match or closes the lobby.

#### Create and update a room

```ts
const permissions = await QubizSDK.requestPermissions(["multiplayer:rooms"]);

if (permissions.status === "granted") {
  const room = await QubizSDK.multiplayer.createRoom({
    mode: "duel",
    maxPlayers: 2,
    player: { ready: true, score: 0 }
  });

  if (room.roomId) {
    await QubizSDK.multiplayer.updateState(room.roomId, {
      phase: "countdown",
      seed: 82741
    });
  }
}
```

#### Send a room event

```ts
await QubizSDK.multiplayer.sendEvent({
  roomId,
  type: "player:dash",
  payload: { x: 240, y: 128 },
  reliable: true
});
```

---

# Cloud, Progress, and Economy

Save data, load data, unlock achievements, update quests, and record economy transactions.

Tags: cloud, achievements, quests, economy, wallet

## Cloud data

The cloud namespace mirrors saveData, loadData, and deleteData with names that read naturally in game code. Use storage:save for writes and storage:read for reads.

#### Save and load

```ts
await QubizSDK.requestPermissions(["storage:save", "storage:read"]);

await QubizSDK.cloud.save("slot-1", {
  checkpoint: "tower-door",
  hp: 7
});

const save = await QubizSDK.cloud.load("slot-1");
console.log(save.status, save.value);
```

## Achievements and quests

Achievements represent durable unlocks. Quests represent progress toward an objective. Keep ids stable so players do not lose progression when a build changes.

#### Progress APIs

```ts
await QubizSDK.requestPermissions([
  "achievements:write",
  "quests:write"
]);

await QubizSDK.achievements.unlock({
  id: "first-clear",
  title: "First clear"
});

await QubizSDK.quests.update({
  id: "daily-run",
  objectiveId: "finish-rounds",
  progress: 3,
  completed: false
});
```

## Economy API

Use the economy namespace for in-game wallet reads and currency/item transactions. The current host keeps this lightweight and status-driven; payment-backed entitlements should still use the payment flow.

#### Wallet and transaction

```ts
await QubizSDK.requestPermissions(["economy:read", "economy:write"]);

const wallet = await QubizSDK.economy.getWallet();

await QubizSDK.economy.transact({
  currency: "coins",
  amount: 120,
  reason: "level_reward",
  metadata: { stage: "world-2" }
});
```

---

# Analytics Events and Sinks

Use trackEvent as a Roblox-Studio-like creator sink for progression, funnels, source attribution, retention, and balancing.

Tags: events, analytics, sinks, trackEvent, retention

## Event shape

trackEvent sends a bounded analytics event for the published game. Creators can see rollups such as event count, unique players, retained players, top event names, and top sources.

Use event names as stable identifiers. Use stage for the current level, mission, room, wave, biome, or tutorial step. Use source for where something came from, such as tutorial, shop, reward, spawn, referral, or enemy type.

- name: required, max 64 characters.
- stage: optional, max 80 characters.
- source: optional, max 80 characters.
- value: optional finite number.
- metadata: optional object, max 12 primitive keys.
- metadata string values: max 120 characters.

#### Track progression

```ts
await QubizSDK.requestPermissions(["analytics:events"]);

await QubizSDK.trackEvent({
  name: "level_reached",
  stage: "world-2-room-4",
  source: "main-path",
  value: 2,
  metadata: {
    difficulty: "normal",
    deaths: 3,
    usedAssist: false
  }
});
```

## Recommended event names

Use a small vocabulary of stable event names. Stable names make dashboards useful; random or overly specific names fragment your data.

- game_started - player reached the playable game loop.
- tutorial_step - player completed or failed a tutorial step.
- level_started - player entered a level, stage, room, or wave.
- level_reached - player reached a milestone inside the progression.
- level_completed - player completed a level or mission.
- run_failed - player lost a run; stage can show where.
- currency_earned - player earned coins, gems, XP, or points.
- currency_spent - player spent a resource; source can show the sink.
- item_acquired - player received an item, skin, ability, or card.
- source_seen - player arrived from a menu, referral, portal, reward, or spawn source.

## Funnels and retention

Retention in the creator dashboard is currently calculated from real event days: a retained player is a player with events on more than one day. That is simple but source-backed.

For useful funnels, send events at the same grain: one event per meaningful step, not every frame. For example, game_started, tutorial_step, level_started, level_completed, run_failed.

#### Track source attribution

```ts
await QubizSDK.trackEvent({
  name: "currency_earned",
  source: "daily_reward",
  value: 100,
  metadata: {
    rewardType: "coins",
    streakDay: 5
  }
});
```

---

# Game Uploads and Manifests

How to prepare an upload package, metadata, SDK scopes, tags, artwork, and creator attestations.

Tags: upload, manifest, creator

## Manifest fields

A game manifest is the public-facing metadata and review record for an uploaded game. Keep it factual and player-readable; do not put secrets, credentials, or private account data in any manifest field.

- slug - lowercase URL id, for example my-first-game.
- title - max 80 characters.
- description - max 600 characters.
- ownerUid - Firebase Auth uid of the creator.
- status - draft, in_review, needs_changes, or published.
- visibility - private or public.
- engine - HTML5, Phaser, Unity WebGL, or Other.
- tags - up to 12 short tags.
- requestedScopes - up to 17 valid SDK permission scopes.
- build, cover, banner - uploaded package and artwork metadata.
- legalAttestations - rights, malware, external calls, and guideline confirmations.

## Upload limits

The current upload wizard accepts a build file up to 100 MB. This keeps uploads fast and review workload under control.

Uploads are saved as drafts first. Creators cannot self-publish. Qubiz publishes after review.

- Build files: max 100 MB.
- Tags: max 12.
- Requested scopes: max 17.
- Creator legal attestations must all be true before upload.
- Published games are the only game pages included in the public sitemap.

---

# CLI Tooling

Use command-line helpers for scaffolding, manifest validation, SDK checks, local preview, and release packaging.

Tags: cli, tooling, manifest, release

## Recommended commands

The JavaScript package exposes a qubiz-game-sdk binary with create and validate commands, while QubizSDK.cli.command produces copyable command strings inside editor tooling. The platform scripts also include sdk:publish for npm/Python package checks and publishing when credentials are present.

- create - scaffold a small HTML5 browser-game project.
- validate - check manifest fields, requested scopes, event names, and asset references.
- preview - serve a local build with the SDK browser bundle.
- pack - create an upload-ready ZIP and asset manifest.
- publish - run package checks and publish SDK packages when credentials are configured.

#### Scaffold and validate

```bash
npx @qubiz/game-sdk create my-game --template arcade
npx @qubiz/game-sdk validate manifest.json
npx @qubiz/game-sdk preview ./dist
npx @qubiz/game-sdk pack ./dist --assets ./assets
```

#### Generate commands from the SDK

```ts
const command = QubizSDK.cli.command("validate", ["manifest.json"]);
console.log(command);
```

---

# Python Creator SDK

Use Python helpers to validate manifests and analytics events before upload.

Tags: python, manifest, validate, pypi

## Install

The Python package is creator tooling. It does not run inside the browser game. Use it for validation scripts, CI checks, upload helpers, and release pipelines.

#### Install from PyPI

```bash
pip install qubiz-game-sdk
```

## Validate a manifest and event

validate_manifest checks slug format, title length, description length, tag limits, scope limits, and known permission scopes. validate_event checks event name, stage, source, numeric value, and metadata limits.

#### Python validation

```py
from qubiz_game_sdk import GameManifest, SDKEvent, validate_event, validate_manifest

manifest = GameManifest(
    slug="my-game",
    title="My Game",
    description="A small HTML5 arcade game.",
    requested_scopes=["profile:username", "analytics:events"],
    tags=["arcade", "short-rounds"],
)
validate_manifest(manifest)

progress_event = SDKEvent(
    name="level_reached",
    stage="world-1",
    source="tutorial",
    value=3,
    metadata={"difficulty": "normal"},
)
validate_event(progress_event)
```

---

# Profiles, Comments, and Reports

Public profiles, profile privacy toggles, comments, and report flow behavior.

Tags: profiles, comments, reports, privacy

## Public profiles

A user can make their profile public from settings. Public profile documents live separately from private users/{uid} documents so email and provider data are not exposed.

Users can independently hide their published games and public comments from their profile.

- publicProfiles/{uid} is readable only when visibility is public, or by the owner.
- users/{uid} remains private to the owner.
- Profile bio is capped at 180 characters.
- Username/display name fields are capped at 32 characters.

## Comments and reports

Published games can show public comments. Signed-in non-anonymous users can post comments and choose whether a comment is public or private.

Reports are API-only. The client sends a signed Firebase ID token to /api/reports; direct public writes to report records are not supported.

- Comment body max: 500 characters.
- Report reason max: 1000 characters.
- Open report rate limit: 5 open/reviewing reports per user.
- Reports support game, comment, profile, and submission targets.

---

# Troubleshooting

What to check when SDK calls, uploads, analytics, comments, or reports do not work.

Tags: debug, errors, limits, firebase

## SDK calls return unavailable

unavailable usually means Firebase, login, or the host bridge is not ready. The game should keep running and show a soft fallback.

For local prototypes, confirm the game is loaded inside a Qubiz host page and targetOrigin matches the host origin.

- Check QubizSDK.configure targetOrigin.
- Check that the player is signed in for write operations.
- Check that the required permission scope is granted.
- Check browser console messages from the host page.

## Upload or publish fails

Most upload failures come from a missing build ZIP, over-large files, incomplete legal attestations, invalid manifest fields, or requested SDK scopes that are not supported.

- Use the upload wizard with a real account, not a guest session.
- Keep the build ZIP under 100 MB.
- Use only valid requestedScopes.
- Submit as a draft and respond to review messages if changes are requested.
- If a public page does not appear in the sitemap, confirm status is published.