# Qortal Q-Apps & QDN Websites — Developer & LLM Guide **Revision:** 2026-07-14 **Audience:** Developers and AI assistants building Q-Apps and WEBSITE resources on [Qortal](https://github.com/qortal) **Scope:** `qortalRequest`, QDN, packaging, Hub deployment, and common production patterns --- ## Navigation | § | Topic | Read when… | |---|--------|------------| | [0](#0-for-llms-start-here) | For LLMs | You are an AI loading this as context | | [1](#1-mental-model) | Mental model | First time on Qortal / QDN | | [2](#2-website-vs-app) | WEBSITE vs APP | Choosing service type or debugging 404 vs SPA routing | | [3](#3-qdn-coordinates) | QDN coordinates | Publishing, fetching, or linking resources | | [4](#4-service-types) | Service types | Picking IMAGE / JSON / DOCUMENT / APP / … | | [5](#5-qortalrequest-bridge) | `qortalRequest` | Wallet, publish, encrypt, navigate | | [6](#6-fetch-and-rest) | `fetch` & REST | Public reads without wallet | | [7](#7-packaging-and-publishing) | Packaging | Zip layout, Vite, grey-screen fixes | | [8](#8-linking-and-deep-links) | Linking | `qortal://`, SPA routing, hash deep links | | [9](#9-hub-integration) | Hub integration | Auth, iframes, mobile, bridge patterns | | [10](#10-ecosystem-and-conventions) | Ecosystem | Cross-app identifiers and reference Q-Apps | | [11](#11-source-repos) | Source repos | Gitea, GitHub, official docs | | [12](#12-pitfalls) | Pitfalls | Debugging checklist | | [13](#13-security) | Security | Trust model | | [14](#14-workflows) | Workflows | Step-by-step new Q-App / WEBSITE | | [15](#15-skeleton) | Skeleton | Copy-paste starter | | [16](#16-llm-system-prompt) | System prompt | Paste into another LLM | --- ## 0. For LLMs: start here 1. Read **§1–§2** before generating architecture — confusing WEBSITE and APP causes most routing bugs. 2. Use **`qortalRequest`** for wallet, signing, publish, encrypt, and payments; use **relative `fetch()`** for anonymous public reads (§6). 3. Before publish/search code: read **§3, §6, §12** (identifiers, search normalization, UTF-8 base64). 4. Before shipping a zip: read **§7** (flat `dist/`, `base: "./"`, no CDN). 5. When the user's Hub/Core version matters, **verify action names** in [qortal.dev/docs/q-apps](https://qortal.dev/docs/q-apps) or Core `q-apps.js` — do not invent parameters. **Default artifact:** single-file HTML for small tools; **Vite + flat zip** for production SPAs. **Authority order when sources conflict:** 1. User's running **Qortal Core + Hub version** 2. [Q-Apps.md](https://github.com/Qortal/qortal/blob/master/Q-Apps.md) in Core 3. [qortal.dev/docs/q-apps](https://qortal.dev/docs/q-apps) 4. This guide (patterns and pitfalls — not a byte-perfect API dump) --- ## 1. Mental model | Term | Meaning | |------|---------| | **Qortal Core** | Node software: chain validation, QDN storage, **REST API** | | **Hub / Mobile / Extension** | User UIs that embed Q-Apps and inject **`qortalRequest`** | | **QDN** | Qortal Data Network — resources indexed on-chain, payload mostly off-chain (SHA-256 verified) | | **Registered name** | On-chain name tied to an account; **required to publish** from a Q-App | | **Resource** | `{ name, service, identifier? }` + data; **latest publish wins** for the same triple | | **Q-App** | Static web app (HTML/JS/CSS), usually a **zip** with **`index.html` at root**, service **`APP`** | | **WEBSITE** | Multi-file site on QDN, service **`WEBSITE`** — different routing (§2) | **Typical Q-App constraints:** no private backend with secrets; identity is **wallet address** + optional **name**; ship assets **in the zip** (offline/CSP-safe). --- ## 2. WEBSITE vs APP | | **WEBSITE** | **APP** | |---|-------------|---------| | **Missing file path** | **404** | Falls through to **`index.html`** (SPA) | | **Use for** | Visitor-facing static or multi-page sites | Interactive apps, builders, marketplaces, client routers | | **Examples** | Portfolio site, published blog layout | Wallet UI, map tool, shop front-end | **Rule:** Client-side router + app behaviour → **`APP`**. Mostly static pages with real 404s for bad URLs → **`WEBSITE`**. **SPA asset trap:** Hub may open `/render/APP/YourName` **without** a trailing slash, so `./assets/index.js` resolves against `/render/APP/` and **404s**. Fixes: - Ensure a **trailing slash** on the app URL, or - Inject `` pointing at the app root (only when you understand the URL shape), or - Use absolute-to-root paths that match how your Hub serves the resource. Rebuild and re-zip after any HTML or bundler config change. --- ## 3. QDN coordinates Every QDN object: - **`name`** — publisher's registered name (controls update rights) - **`service`** — `IMAGE`, `JSON`, `APP`, `WEBSITE`, `DOCUMENT`, `BLOG_POST`, … - **`identifier`** — optional; distinguishes multiple resources per name+service **Default resource:** main website or app for a name — omit `identifier` or use `"default"`. **Shared identifiers:** deterministic cross-app locations (e.g. avatar: service `THUMBNAIL`, identifier `qortal_avatar` + user `name`). **App namespaces:** prefix identifiers with your app slug: `myapp_config_v1`, `myapp_post_`. QDN is permissionless — **validate payload schema**; never trust identifier alone. --- ## 4. Service types Condensed from [qortal.dev](https://qortal.dev/docs/q-apps). Sizes are per-service limits where documented. | Service | Role | Typical limit | |---------|------|----------------| | `JSON` | Small structured metadata | **25 KB** | | `DOCUMENT` | Larger text/JSON documents | 500 MB general cap | | `DOCUMENT_PRIVATE` (801) | Encrypted documents | Private | | `IMAGE` / `THUMBNAIL` | Media | 10 MB / 500 KB | | `FILE` / `VIDEO` / `AUDIO` | Binaries | Large | | `WEBSITE` / `APP` | Multi-file zips | APP **50 MB** | | `BLOG_POST` / `MAIL` / `MESSAGE` | Ecosystem apps | varies | | `CHAIN_COMMENT` | Tiny on-chain payload | **239 B** | **Single-file vs multi-file:** `JSON`, `IMAGE`, etc. → one payload (`data64` or `file`). `WEBSITE`, `APP`, `GIF_REPOSITORY` → many files; **`filepath`** required on fetch. **Publishing multi-file WEBSITE/APP from inside a Q-App:** still **limited or unavailable** in many Hub builds — publish via Hub UI; **fetch** multi-file resources **is** supported. Verify your target version. **Private services** (`*_PRIVATE`, numeric 801, etc.): encrypted for recipient; decrypt with wallet via `DECRYPT_DATA` or group variants. --- ## 5. `qortalRequest` bridge Injected by Core/Hub into embedded pages. Returns a **Promise** — always **`try/catch`**. Timeouts are **per-action** in Core `q-apps.js`; fix payload size and batching rather than wrapping everything in long timeouts. **Host variables:** `window._qdnTheme` (`light`|`dark`), `window._qdnContext` (e.g. `gateway`). ### 5.1 Feature detection ```javascript async function qr(payload) { const req = window.qortalRequest ?? window.parent?.qortalRequest; if (typeof req !== "function") throw new Error("Open inside Qortal Hub"); return req(payload); } ``` Resolve the bridge from `window`, then `parent`, then `top` when running in nested iframes. ### 5.2 Identity (gesture-gated) | Action | Purpose | |--------|---------| | `GET_USER_ACCOUNT` | Wallet address + public key — **user approval**; call from a **button**, not on first paint | | `GET_ACCOUNT_NAMES` | Names for an address | | `GET_ACCOUNT_DATA` | Account record | | `GET_NAME_DATA` | Name owner and metadata | **Publish gate:** user must **own** a registered name. ### 5.3 QDN read | Action | Purpose | |--------|---------| | `FETCH_QDN_RESOURCE` | Load resource; add `filepath` for multi-file; `encoding: "base64"` when needed | | `SEARCH_QDN_RESOURCES` | Discover resources; use `prefix: true` for identifier prefixes; **minimal payload** | | `LIST_QDN_RESOURCES` | List by name/service (richer filters than search in some builds) | | `GET_QDN_RESOURCE_URL` | URL for `` / fetch (e.g. `/arbitrary/...`) | | `GET_QDN_RESOURCE_STATUS` | Build/load status (`READY`, percent loaded) — probe without full fetch | | `GET_QDN_RESOURCE_METADATA` | Title, tags, category without full payload | | `GET_QDN_RESOURCE_PROPERTIES` | filename, mimeType, size | **Search normalization:** cores return array **or** `{ resources | results | entries | hits | data: [...] }` — coerce to array. Unknown JSON keys on search → **empty results**; strip optional fields if search mysteriously fails. ### 5.4 QDN write | Action | Purpose | |--------|---------| | `PUBLISH_QDN_RESOURCE` | Single file / `data64`; `name` must be owned | | `PUBLISH_MULTIPLE_QDN_RESOURCES` | Batch publish — feature-detect; fewer approval rounds | **UTF-8 JSON → base64** — never raw `btoa(JSON.stringify(obj))` on Unicode text: ```javascript function b64Utf8(obj) { const bytes = new TextEncoder().encode(JSON.stringify(obj)); let bin = ""; for (const b of bytes) bin += String.fromCharCode(b); return btoa(bin); } ``` ### 5.5 Navigation & Hub UI | Action / pattern | Purpose | |------------------|---------| | `LINK_TO_QDN_RESOURCE` | Open QDN resource in Hub | | `OPEN_NEW_TAB` | New tab with URL or `qortal://` link | | `OPEN_PROFILE` | Open peer profile | | `SET_TAB` (via `postMessage`) | Hub tab switch — `{ action: 'SET_TAB', requestedHandler: 'UI', payload: { service, name, identifier?, path? } }` | | `QDN_RESOURCE_DISPLAYED` | Tell Hub what resource is shown (address bar / copy-link) — `{ action, service, name, path? }` | | `SET_TAB_NOTIFICATIONS` | Badge count on tab | | `NAVIGATION_HISTORY` | Back/forward integration (Hub-dependent) | ### 5.6 Groups, polls, payments | Action | Notes | |--------|-------| | `LIST_GROUPS` / `JOIN_GROUP` / `GET_GROUPS_WITH_MEMBER` | Group membership | | `CREATE_POLL` / `VOTE_ON_POLL` | `pollOptions` must be **array**; `pollName` must be unique | | `SEND_COIN` / `TRANSFER_ASSET` / `SEND_PAYMENT` | User approval; **confirm params** on target Hub version | | `SIGN_TRANSACTION` | Hub-signed tx, then broadcast via REST `/transactions/process` | | `SEARCH_TRANSACTIONS` | Payment matching — `message` field often missing; match by reference/type | ### 5.7 Encryption | Action | Purpose | |--------|---------| | `ENCRYPT_DATA` | `file` or `base64`; optional `publicKeys` for group | | `DECRYPT_DATA` | Ciphertext base64 + counterparty `publicKey` | | `ENCRYPT_QORTAL_GROUP_DATA` / `DECRYPT_QORTAL_GROUP_DATA` | Group-scoped encryption | Use **separate identifier namespaces** for encrypted blobs. ### 5.8 Lists (local UI state) `GET_LIST_ITEMS`, `ADD_LIST_ITEMS`, `DELETE_LIST_ITEM` — per-user lists in Hub, not global QDN. ### 5.9 Mobile (Qortal Go) | Action / URL | Purpose | |--------------|---------| | `SAVE_FILE` | Save `blob` + `filename` natively — mobile WebViews often **ignore** anchor/blob downloads | | `/arbitrary/…?attachment=true` | Native download listener on some mobile builds | Detect mobile via user-agent when choosing download strategy. ### 5.10 Chain & misc `FETCH_BLOCK`, `GET_BALANCE`, `GET_BLOCK_HEIGHT`, `GET_PRICE`, `DEPLOY_AT`, `GET_AT`, notifications permission actions — see [qortal.dev](https://qortal.dev/docs/q-apps) for the full list. --- ## 6. `fetch` and REST Inside Hub, **relative** `fetch("/names/foo")` hits the user's node API — good for **public reads** (blocks, names, arbitrary endpoints). **Limitation:** `fetch` does not know the logged-in wallet; personalized or signing flows need **`qortalRequest`**. Do **not** hardcode `http://127.0.0.1:12391` in shipped apps. **QDN search modes (REST):** `mode=ALL` vs `mode=LATEST` — wrong mode → duplicates or wrong counts. **Reference:** [api.qortal.org/api-documentation](https://api.qortal.org/api-documentation/) — cross-check field names when debugging bridge vs REST. **Local preview (varies by Core):** `http://localhost:12391/render/APP/?preview=true` --- ## 7. Packaging and publishing ### 7.1 Zip layout (non-negotiable) ``` my-app.zip ├── index.html ← at zip root ├── manifest.json ├── assets/ │ ├── index-xxxxx.js │ └── index-xxxxx.css └── … (fonts, images, workers) ``` - Zip **contents** of `dist/`, not a `dist/` folder wrapper. - **`manifest.json`:** `"main": "index.html"`, title, version, category. - Paths: **`./assets/...`**, never `/assets/...`. - Avoid `` unless you are deliberately fixing APP path resolution (§2). - Vite: `base: "./"`. - **Ship fonts/JS in-bundle** — no required CDN. - Case-sensitive paths; prefer `[a-z0-9._-]`. ### 7.2 Workflow 1. `npm run build` → `dist/` (or equivalent) 2. `(cd dist && zip -r ../MyApp.zip .)` 3. Hub → load zip → **Preview** before QDN publish 4. Publish service **`APP`** (or **`WEBSITE`** via Hub for end-user sites) 5. Open `qortal://APP/` or `qortal://WEBSITE//` ### 7.3 Grey screen / eternal “Loading…” | Cause | Fix | |-------|-----| | `assets/index-*.js` 404 | Flat zip, `base: "./"`, rebuild | | Absolute `/assets/` paths | Relative `./assets/` | | Stale hashed files | Full rebuild + fresh zip | | `crossorigin` on scripts | Some Hub builds fail script load — remove if needed | --- ## 8. Linking and deep links ### 8.1 `qortal://` protocol ```text qortal://{service}/{name}/{identifier?}/{path?} ``` Examples: - `qortal://WEBSITE/MySite` — home - `qortal://WEBSITE/MySite/gallery` — sub-page - `qortal://APP/MyQApp` - `qortal://THUMBNAIL/MyName/qortal_avatar` Use `default` in path when you need explicit no-identifier semantics. ### 8.2 SPA routing inside Hub Published apps often run under paths like `/render/WEBSITE//` or `/render/APP//`. Use **pathname** segments for sub-pages and **`history.pushState` / `popstate`** for in-app navigation without leaving the Hub. **Hash deep links (optional):** SPAs may use `location.hash` for anchors (e.g. `#section-id`). If you use multiple hash prefixes (e.g. `#folder-…` and `#folderdir-…`), parse **longest match first** — naive `startsWith('folder-')` breaks when one prefix is a substring of another. Preserve intentional hash fragments when calling `history.replaceState` for pathname cleanup. ### 8.3 Programmatic navigation Prefer `LINK_TO_QDN_RESOURCE` or `SET_TAB` postMessage for cross-app opens. --- ## 9. Hub integration ### 9.1 Authentication - `GET_USER_ACCOUNT` only works **inside Hub** (or via a bridge stub that forwards to `parent`). - Opening built `index.html` in a normal browser **cannot** complete wallet auth. - If `parent === top` and only a stub exists, wallet calls fail — user must open the app from Hub. - **Do not** auto-call `GET_USER_ACCOUNT` on page load; use an explicit **Connect** / **Authenticate** button. - **Embedded Q-Apps in iframes:** parent page may need a **message relay** that forwards `qortalRequest` to the Hub; if inner auth still fails, open the target in a **new Hub tab**. ### 9.2 Bridge wrapper pattern Centralize all bridge calls in one module. Typical resolution order: ```text window.qortalRequest → parent.qortalRequest → top.qortalRequest ``` Wrap with short-timeout and long-timeout helpers (publishes and payments often need longer limits). Always `try/catch`. ### 9.3 Preview mode ```javascript if (typeof qortalRequest !== "function" && typeof window.parent?.qortalRequest !== "function") { // Show "Preview mode" — mock reads only; never fake successful payments } ``` ### 9.4 Hub copy-link / address bar Multi-page WEBSITE apps can notify the Hub what sub-path is displayed via `QDN_RESOURCE_DISPLAYED`, so copy-link matches the page the user sees. Optionally maintain `` with the matching `qortal://WEBSITE//` form. --- ## 10. Ecosystem and conventions Well-known **reference Q-Apps** on GitHub (study zip layout and `qortalRequest` usage): | Project | Repo | Typical services | |---------|------|------------------| | Q-Mail | [Qortal/q-mail](https://github.com/Qortal/q-mail) | `MAIL`, `MAIL_PRIVATE` | | Q-Share | [Qortal/q-share](https://github.com/Qortal/q-share) | `FILE`, `DOCUMENT` | | Q-Tube | [Qortal/q-tube](https://github.com/Qortal/q-tube) | `VIDEO`, `BLOG_POST` | | Q-Shop | [Qortal/q-shop](https://github.com/Qortal/q-shop) | `STORE`, `PRODUCT`, payments | **Cross-app conventions:** | Identifier / pattern | Used for | |---------------------|----------| | `qortal_avatar` + `THUMBNAIL` | User avatar image | | `myapp_` prefix on identifiers | App-scoped JSON search | | `BLOG_POST` + Q-Blog identifiers | Blog content blocks | | `FILE` + `qfile_…` identifiers | File attachments in blog/mail apps | **Multi-app ecosystems:** Some products publish a **WEBSITE** for visitors plus separate **APP** tools plus **JSON/DOCUMENT** indexes for discovery. Use versioned schema strings in JSON (e.g. `myapp-index-v1`) and document identifiers in your app's README. --- ## 11. Source repos ### 11.1 Gitea (community) | URL | Use | |-----|-----| | [gitea.qortal.link](https://gitea.qortal.link) | Community Gitea — browse and host Qortal-related projects | Search Gitea for Q-Apps, Hub forks, and site builders. Clone URLs follow `https://gitea.qortal.link//.git`. ### 11.2 GitHub (official org) | Repo | Use | |------|-----| | [Qortal/qortal](https://github.com/Qortal/qortal) | Core, **`Q-Apps.md`**, **`q-apps.js`** (timeouts, actions) | | [Qortal/Qortal-Hub](https://github.com/Qortal/Qortal-Hub) | Desktop Hub | | [Qortal/qapp-core](https://github.com/Qortal/qapp-core) | Optional npm helpers | | [Qortal/q-mail](https://github.com/Qortal/q-mail), [q-share](https://github.com/Qortal/q-share), [q-tube](https://github.com/Qortal/q-tube), [q-shop](https://github.com/Qortal/q-shop) | Reference apps | ### 11.3 Official documentation | URL | Use | |-----|-----| | [qortal.dev/docs/q-apps](https://qortal.dev/docs/q-apps) | **`qortalRequest`** reference (authoritative for actions) | | [api.qortal.org/api-documentation](https://api.qortal.org/api-documentation/) | REST / node API | | [Q-Apps.md](https://github.com/Qortal/qortal/blob/master/Q-Apps.md) | Protocol: routing, resources, linking | --- ## 12. Pitfalls | Problem | Fix | |---------|-----| | Grey screen / Loading forever | Network tab: `assets/index-*.js` must be **200**; flat zip, `base: "./"` | | `/assets/...` 404 on APP | Trailing slash / `` / relative paths | | Auto `GET_USER_ACCOUNT` on load | Use **Connect** button | | `btoa(JSON.stringify)` on Unicode | §5.4 UTF-8 safe base64 | | `SEARCH_*` not an array | Normalize to array | | Extra fields on search → `[]` | Minimal search payload | | `JSON` service > 25 KB | Use `FILE` / `DOCUMENT` | | Unprefixed identifiers | `appslug_type_id` | | Multi-file publish from Q-App | Use Hub UI; verify version | | `CREATE_POLL` options as string | **Array** of `{ optionName }` | | Hardcoded localhost API | Relative `fetch` only | | External CDN for core JS | Bundle in zip | | Unsanitized QDN HTML → XSS | Escape / sanitize | | `Promise.all` on hundreds of fetches | Bounded concurrency | | Hub iframe wallet isolation | Open in new tab / message relay | | Mobile blob download | `SAVE_FILE` or `?attachment=true` | | Overlapping hash prefixes | Parse longest prefix first | | Payment succeeded, notify failed | Treat payment and notification as separate steps | --- ## 13. Security - **Never** collect seed phrases or private keys. - Treat all QDN JSON as **untrusted** — validate schema and sizes. - Explain publishes, payments, and decrypt actions **before** calling `qortalRequest`. - Rate-limit retries; distinguish user cancel vs technical errors. --- ## 14. Workflows ### 14.1 New Q-App 1. Choose stack: single HTML vs Vite/React (or similar). 2. Scaffold `index.html`, `manifest.json`, relative paths only. 3. Central bridge module for all `qortalRequest` calls. 4. Connect button → `GET_USER_ACCOUNT` → `GET_ACCOUNT_NAMES` → gate publish. 5. Design identifier scheme + JSON schemas. 6. Reads: `FETCH_QDN_RESOURCE` / `SEARCH_QDN_RESOURCES` with normalization. 7. Writes: `PUBLISH_QDN_RESOURCE` with UTF-8 `data64`. 8. Test: root URL, deep link, no-name wallet, publish propagation delay. 9. Build → flat zip → Hub Preview → publish `APP`. ### 14.2 New WEBSITE (visitor site) 1. Static HTML/CSS/JS, relative paths. 2. Publish via Hub under an owned name → service **`WEBSITE`**. 3. Link: `qortal://WEBSITE//...` 4. If the product needs SPA routing and heavy `qortalRequest` use throughout, consider **`APP`** instead (§2). ### 14.3 Inter-app integration 1. Define stable **identifier prefixes** and JSON schema versions. 2. Publish discovery metadata as small **JSON** or **DOCUMENT** resources. 3. Read partner apps via `SEARCH_QDN_RESOURCES` with `prefix: true`. 4. Open partner resources with `qortal://` links or `LINK_TO_QDN_RESOURCE`. 5. Never assume identifier alone proves origin — validate payload shape. --- ## 15. Skeleton Minimal single-file Q-App (teaching reference): ```html My Q-App

My Q-App


  


```

---

## 16. LLM system prompt

Paste as system instructions for another model:

```text
You build Qortal Q-Apps: static web apps using qortalRequest for wallet/QDN/signing and relative fetch() for public Core reads.

Official API: https://qortal.dev/docs/q-apps — verify action names against the user's Hub/Core version.
Protocol reference: https://github.com/Qortal/qortal/blob/master/Q-Apps.md
REST reference: https://api.qortal.org/api-documentation/
Community repos: https://gitea.qortal.link and https://github.com/qortal

Rules:
- No  unless deliberately fixing Hub APP path resolution.
- Relative paths (./assets/...) and bundler base "./".
- index.html at zip root; ship fonts/JS in-bundle; no required CDN.
- GET_USER_ACCOUNT only after explicit user gesture. No registered name → explain publish cannot proceed.
- Prefix QDN identifiers with app slug; validate all fetched JSON; UTF-8 safe base64 for publishes.
- Normalize SEARCH_QDN_RESOURCES to an array; minimal search payload if results empty.
- JSON service ≤25 KB; large data → FILE/IMAGE/DOCUMENT.
- try/catch all qortalRequest; never handle seeds/private keys.
- Preview mode when bridge missing — mock reads only, never fake payments.
- WEBSITE = 404 on bad paths; APP = SPA fallback to index.html.
- Mobile: SAVE_FILE for blob downloads when anchor download fails.
```

---

## Maintenance

- Bump **revision date** when Core/Hub changes `qortalRequest` contracts materially.
- Re-check multi-file publish support after major Hub releases.
- Keep links to [qortal.dev](https://qortal.dev/docs/q-apps), [api.qortal.org](https://api.qortal.org/api-documentation/), and [gitea.qortal.link](https://gitea.qortal.link) current.

---

*End of guide.*