+
+`
diff --git a/portal/internal/web/docs/api/lua-api.md b/portal/internal/web/docs/api/lua-api.md
new file mode 100644
index 0000000..c4a6063
--- /dev/null
+++ b/portal/internal/web/docs/api/lua-api.md
@@ -0,0 +1,806 @@
+# Lua API Reference
+
+Complete reference for the Mosis Lua API available to apps.
+
+## Global Objects
+
+### document
+
+The current RML document. Use to query and modify UI elements.
+
+```lua
+-- Get element by ID
+local elem = document:GetElementById("my-id")
+
+-- Get elements by tag
+local buttons = document:GetElementsByTagName("button")
+
+-- Get elements by class
+local cards = document:GetElementsByClassName("card")
+```
+
+### event
+
+Available in event handler functions. Contains information about the triggering event.
+
+```lua
+function handleClick(event)
+ local target = event:GetCurrentElement()
+ local eventType = event.type
+end
+```
+
+## Document Methods
+
+### GetElementById(id)
+
+Returns the element with the specified ID, or `nil` if not found.
+
+```lua
+local element = document:GetElementById("username-input")
+if element then
+ element.inner_rml = "Found!"
+end
+```
+
+### GetElementsByTagName(tag)
+
+Returns a table of all elements with the specified tag name.
+
+```lua
+local buttons = document:GetElementsByTagName("button")
+for i, btn in ipairs(buttons) do
+ btn:SetClass("styled", true)
+end
+```
+
+### GetElementsByClassName(class)
+
+Returns a table of all elements with the specified class name.
+
+```lua
+local items = document:GetElementsByClassName("list-item")
+```
+
+### CreateElement(tag)
+
+Creates a new element with the specified tag name.
+
+```lua
+local div = document:CreateElement("div")
+div.inner_rml = "New element"
+parent:AppendChild(div)
+```
+
+### CreateTextNode(text)
+
+Creates a text node with the specified content.
+
+```lua
+local text = document:CreateTextNode("Hello")
+element:AppendChild(text)
+```
+
+## Element Properties
+
+### inner_rml
+
+Gets or sets the inner RML content of an element.
+
+```lua
+-- Get content
+local content = element.inner_rml
+
+-- Set content (parses RML)
+element.inner_rml = "Bold text"
+```
+
+### id
+
+Gets or sets the element's ID.
+
+```lua
+local id = element.id
+element.id = "new-id"
+```
+
+### style
+
+Access to the element's inline styles.
+
+```lua
+element.style.width = "100dp"
+element.style.backgroundColor = "#ff0000"
+element.style.display = "none"
+```
+
+### parent_node
+
+Returns the parent element, or `nil` if none.
+
+```lua
+local parent = element.parent_node
+```
+
+### first_child / last_child
+
+Returns the first or last child element.
+
+```lua
+local first = container.first_child
+local last = container.last_child
+```
+
+### next_sibling / previous_sibling
+
+Returns the next or previous sibling element.
+
+```lua
+local next = element.next_sibling
+```
+
+### child_nodes
+
+Returns a table of all child elements.
+
+```lua
+local children = element.child_nodes
+for i, child in ipairs(children) do
+ print(child.id)
+end
+```
+
+### tag_name
+
+Returns the element's tag name (lowercase).
+
+```lua
+local tag = element.tag_name -- "div", "button", etc.
+```
+
+### offset_width / offset_height
+
+Returns the rendered dimensions of the element.
+
+```lua
+local width = element.offset_width
+local height = element.offset_height
+```
+
+### offset_left / offset_top
+
+Returns the position relative to the offset parent.
+
+```lua
+local x = element.offset_left
+local y = element.offset_top
+```
+
+## Element Methods
+
+### GetAttribute(name)
+
+Returns the value of the specified attribute.
+
+```lua
+local value = input:GetAttribute("value")
+local placeholder = input:GetAttribute("placeholder")
+```
+
+### SetAttribute(name, value)
+
+Sets the value of the specified attribute.
+
+```lua
+input:SetAttribute("placeholder", "Enter text...")
+button:SetAttribute("disabled", "disabled")
+```
+
+### RemoveAttribute(name)
+
+Removes the specified attribute.
+
+```lua
+button:RemoveAttribute("disabled")
+```
+
+### HasAttribute(name)
+
+Returns `true` if the element has the specified attribute.
+
+```lua
+if button:HasAttribute("disabled") then
+ print("Button is disabled")
+end
+```
+
+### SetClass(name, add)
+
+Adds or removes a class from the element.
+
+```lua
+-- Add class
+element:SetClass("active", true)
+
+-- Remove class
+element:SetClass("active", false)
+```
+
+### IsClassSet(name)
+
+Returns `true` if the element has the specified class.
+
+```lua
+if element:IsClassSet("selected") then
+ print("Element is selected")
+end
+```
+
+### AppendChild(element)
+
+Appends a child element.
+
+```lua
+local child = document:CreateElement("div")
+parent:AppendChild(child)
+```
+
+### InsertBefore(element, reference)
+
+Inserts an element before the reference element.
+
+```lua
+parent:InsertBefore(newElement, referenceElement)
+```
+
+### RemoveChild(element)
+
+Removes a child element.
+
+```lua
+parent:RemoveChild(childElement)
+```
+
+### Focus()
+
+Sets focus to the element.
+
+```lua
+input:Focus()
+```
+
+### Blur()
+
+Removes focus from the element.
+
+```lua
+input:Blur()
+```
+
+### Click()
+
+Simulates a click on the element.
+
+```lua
+button:Click()
+```
+
+### ScrollIntoView(alignToTop)
+
+Scrolls the element into view.
+
+```lua
+element:ScrollIntoView(true) -- align to top
+element:ScrollIntoView(false) -- align to bottom
+```
+
+### AddEventListener(event, handler)
+
+Adds an event listener to the element.
+
+```lua
+button:AddEventListener("click", function(event)
+ print("Clicked!")
+end)
+```
+
+### RemoveEventListener(event, handler)
+
+Removes an event listener from the element.
+
+```lua
+local handler = function(event) print("Click") end
+button:AddEventListener("click", handler)
+button:RemoveEventListener("click", handler)
+```
+
+## Event Object
+
+### type
+
+The event type string (e.g., "click", "change").
+
+```lua
+if event.type == "click" then
+ -- handle click
+end
+```
+
+### target_element
+
+The element that originally triggered the event.
+
+```lua
+local target = event.target_element
+```
+
+### current_element
+
+The element the event handler is attached to.
+
+```lua
+local current = event.current_element
+```
+
+### GetCurrentElement()
+
+Returns the current element (same as `current_element`).
+
+```lua
+local elem = event:GetCurrentElement()
+```
+
+### StopPropagation()
+
+Stops the event from bubbling up to parent elements.
+
+```lua
+event:StopPropagation()
+```
+
+### StopImmediatePropagation()
+
+Stops the event and prevents other handlers on the same element.
+
+```lua
+event:StopImmediatePropagation()
+```
+
+### parameters
+
+Table containing event-specific parameters.
+
+```lua
+-- Mouse events
+local x = event.parameters.mouse_x
+local y = event.parameters.mouse_y
+local button = event.parameters.button -- 0=left, 1=right, 2=middle
+
+-- Keyboard events
+local key = event.parameters.key_identifier
+local ctrl = event.parameters.ctrl_key
+local shift = event.parameters.shift_key
+local alt = event.parameters.alt_key
+```
+
+## Navigation
+
+### navigateTo(screen)
+
+Navigates to a screen, pushing to history.
+
+```lua
+navigateTo("settings") -- loads assets/settings.rml
+navigateTo("screens/profile") -- loads assets/screens/profile.rml
+```
+
+### goBack()
+
+Navigates back to the previous screen.
+
+```lua
+goBack()
+```
+
+### goHome()
+
+Navigates to the home screen, clearing history.
+
+```lua
+goHome()
+```
+
+### replaceTo(screen)
+
+Replaces current screen without adding to history.
+
+```lua
+replaceTo("login") -- no back navigation possible
+```
+
+### canGoBack()
+
+Returns `true` if there's a previous screen in history.
+
+```lua
+if canGoBack() then
+ backButton.style.display = "block"
+else
+ backButton.style.display = "none"
+end
+```
+
+## Timers
+
+### setTimeout(callback, delay)
+
+Executes callback once after delay (milliseconds). Returns timer ID.
+
+```lua
+local id = setTimeout(function()
+ print("Executed after 1 second")
+end, 1000)
+```
+
+### clearTimeout(id)
+
+Cancels a timeout.
+
+```lua
+local id = setTimeout(callback, 1000)
+clearTimeout(id)
+```
+
+### setInterval(callback, interval)
+
+Executes callback repeatedly. Returns timer ID.
+
+```lua
+local id = setInterval(function()
+ updateClock()
+end, 1000)
+```
+
+### clearInterval(id)
+
+Cancels an interval.
+
+```lua
+clearInterval(intervalId)
+```
+
+## Storage
+
+Persistent key-value storage. Data persists between app sessions.
+
+### storage.set(key, value)
+
+Stores a value. Value can be string, number, boolean, or table.
+
+```lua
+storage.set("username", "alice")
+storage.set("settings", { darkMode = true, fontSize = 16 })
+storage.set("highScore", 1000)
+```
+
+### storage.get(key)
+
+Retrieves a stored value, or `nil` if not found.
+
+```lua
+local username = storage.get("username")
+local settings = storage.get("settings")
+if settings then
+ print(settings.darkMode)
+end
+```
+
+### storage.remove(key)
+
+Removes a stored value.
+
+```lua
+storage.remove("tempData")
+```
+
+### storage.clear()
+
+Removes all stored values.
+
+```lua
+storage.clear()
+```
+
+### storage.keys()
+
+Returns a table of all storage keys.
+
+```lua
+local keys = storage.keys()
+for i, key in ipairs(keys) do
+ print(key)
+end
+```
+
+## HTTP (requires `network` permission)
+
+### http.get(url, callback)
+
+Makes a GET request.
+
+```lua
+http.get("https://api.example.com/data", function(response)
+ if response.ok then
+ local data = json.decode(response.body)
+ print(data.message)
+ else
+ print("Error: " .. response.status)
+ end
+end)
+```
+
+### http.post(url, options, callback)
+
+Makes a POST request.
+
+```lua
+http.post("https://api.example.com/submit", {
+ headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer token123"
+ },
+ body = json.encode({ name = "test" })
+}, function(response)
+ print("Status: " .. response.status)
+end)
+```
+
+### http.request(options, callback)
+
+Makes a custom HTTP request.
+
+```lua
+http.request({
+ method = "PUT",
+ url = "https://api.example.com/resource/1",
+ headers = { ["Content-Type"] = "application/json" },
+ body = json.encode({ updated = true }),
+ timeout = 5000 -- milliseconds
+}, function(response)
+ print(response.status)
+end)
+```
+
+### Response Object
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ok` | boolean | `true` if status is 200-299 |
+| `status` | number | HTTP status code |
+| `statusText` | string | Status message |
+| `headers` | table | Response headers |
+| `body` | string | Response body |
+
+## JSON
+
+### json.encode(value)
+
+Converts a Lua value to a JSON string.
+
+```lua
+local str = json.encode({
+ name = "Alice",
+ items = {"a", "b", "c"},
+ count = 3
+})
+-- '{"name":"Alice","items":["a","b","c"],"count":3}'
+```
+
+### json.decode(str)
+
+Parses a JSON string into a Lua value.
+
+```lua
+local data = json.decode('{"name":"Alice","age":25}')
+print(data.name) -- "Alice"
+print(data.age) -- 25
+```
+
+## Logging
+
+### print(...)
+
+Outputs to the debug console. Accepts multiple arguments.
+
+```lua
+print("Debug message")
+print("Value:", someValue, "Count:", count)
+```
+
+### console.log(...)
+
+Alias for `print()`.
+
+```lua
+console.log("Hello")
+```
+
+### console.warn(...)
+
+Logs a warning message.
+
+```lua
+console.warn("Something might be wrong")
+```
+
+### console.error(...)
+
+Logs an error message.
+
+```lua
+console.error("Something went wrong:", errorMessage)
+```
+
+## Utility Functions
+
+### tostring(value)
+
+Converts a value to a string.
+
+```lua
+local str = tostring(123) -- "123"
+```
+
+### tonumber(value)
+
+Converts a value to a number.
+
+```lua
+local num = tonumber("123") -- 123
+local invalid = tonumber("abc") -- nil
+```
+
+### type(value)
+
+Returns the type of a value as a string.
+
+```lua
+type("hello") -- "string"
+type(123) -- "number"
+type(true) -- "boolean"
+type({}) -- "table"
+type(nil) -- "nil"
+type(print) -- "function"
+```
+
+### pairs(table)
+
+Iterator for all key-value pairs.
+
+```lua
+for key, value in pairs(myTable) do
+ print(key, value)
+end
+```
+
+### ipairs(table)
+
+Iterator for array elements (integer keys starting from 1).
+
+```lua
+for index, value in ipairs(myArray) do
+ print(index, value)
+end
+```
+
+### pcall(func, ...)
+
+Calls a function in protected mode (catches errors).
+
+```lua
+local success, result = pcall(function()
+ return json.decode(maybeInvalidJson)
+end)
+
+if success then
+ print("Parsed:", result)
+else
+ print("Error:", result)
+end
+```
+
+## Standard Libraries
+
+### string
+
+```lua
+string.len(s) -- length
+string.upper(s) -- uppercase
+string.lower(s) -- lowercase
+string.sub(s, i, j) -- substring
+string.find(s, pattern) -- find pattern
+string.gsub(s, pattern, repl) -- replace
+string.match(s, pattern) -- match pattern
+string.format(fmt, ...) -- format string
+string.byte(s, i) -- character code
+string.char(...) -- character from code
+string.rep(s, n) -- repeat string
+string.reverse(s) -- reverse string
+string.split(s, sep) -- split by separator (extension)
+string.trim(s) -- trim whitespace (extension)
+```
+
+### math
+
+```lua
+math.abs(x) -- absolute value
+math.ceil(x) -- round up
+math.floor(x) -- round down
+math.round(x) -- round to nearest (extension)
+math.max(...) -- maximum
+math.min(...) -- minimum
+math.sqrt(x) -- square root
+math.pow(x, y) -- power
+math.exp(x) -- e^x
+math.log(x) -- natural log
+math.sin(x) -- sine
+math.cos(x) -- cosine
+math.tan(x) -- tangent
+math.asin(x) -- arc sine
+math.acos(x) -- arc cosine
+math.atan(x) -- arc tangent
+math.atan2(y, x) -- arc tangent of y/x
+math.deg(x) -- radians to degrees
+math.rad(x) -- degrees to radians
+math.random() -- random 0-1
+math.random(n) -- random 1-n
+math.random(m, n) -- random m-n
+math.randomseed(x) -- set random seed
+math.pi -- 3.14159...
+math.huge -- infinity
+```
+
+### table
+
+```lua
+table.insert(t, value) -- append
+table.insert(t, pos, value) -- insert at position
+table.remove(t) -- remove last
+table.remove(t, pos) -- remove at position
+table.sort(t) -- sort ascending
+table.sort(t, comp) -- sort with comparator
+table.concat(t, sep) -- join to string
+table.unpack(t) -- unpack to values (extension)
+table.pack(...) -- pack values to table (extension)
+```
+
+### os
+
+```lua
+os.time() -- current timestamp
+os.time(t) -- timestamp from table
+os.date() -- current date string
+os.date(format) -- formatted date
+os.date(format, t) -- formatted date for timestamp
+os.date("*t") -- date as table
+os.difftime(t2, t1) -- time difference
+os.clock() -- CPU time used
+```
+
+Date format codes:
+- `%Y` - 4-digit year
+- `%m` - month (01-12)
+- `%d` - day (01-31)
+- `%H` - hour (00-23)
+- `%M` - minute (00-59)
+- `%S` - second (00-59)
+- `%a` - abbreviated weekday
+- `%A` - full weekday
+- `%b` - abbreviated month
+- `%B` - full month
+
+## See Also
+
+- [Lua Scripting Guide](../guides/lua-scripting.md) - Tutorials and examples
+- [Permissions Guide](../guides/permissions.md) - Permission system
+- [UI Design Guide](../guides/ui-design.md) - RML/RCSS reference
diff --git a/portal/internal/web/docs/api/manifest.md b/portal/internal/web/docs/api/manifest.md
new file mode 100644
index 0000000..244c7c5
--- /dev/null
+++ b/portal/internal/web/docs/api/manifest.md
@@ -0,0 +1,341 @@
+# Manifest Reference
+
+Every Mosis app requires a `manifest.json` file in the root of the package. This file describes your app and its requirements.
+
+## Complete Schema
+
+```json
+{
+ "id": "com.example.myapp",
+ "name": "My App",
+ "version": "1.0.0",
+ "version_code": 1,
+ "entry": "assets/main.rml",
+ "permissions": [],
+ "min_mosis_version": "1.0.0",
+ "author": {
+ "name": "Developer Name",
+ "email": "dev@example.com",
+ "url": "https://example.com"
+ },
+ "icons": {
+ "32": "icons/icon-32.png",
+ "64": "icons/icon-64.png",
+ "128": "icons/icon-128.png"
+ },
+ "description": "A short description of your app",
+ "category": "utilities",
+ "screenshots": [
+ "screenshots/1.png",
+ "screenshots/2.png"
+ ],
+ "locales": {
+ "default": "en",
+ "supported": ["en", "es", "fr"]
+ }
+}
+```
+
+## Required Fields
+
+### id
+
+**Type:** `string`
+
+Unique package identifier in reverse domain notation. Must match the ID registered in the developer portal.
+
+```json
+"id": "com.yourcompany.appname"
+```
+
+Rules:
+- Lowercase letters, numbers, and periods only
+- Must have at least two segments (e.g., `com.app`)
+- Maximum 255 characters
+- Cannot start or end with a period
+
+### name
+
+**Type:** `string`
+
+Display name shown to users. Maximum 50 characters.
+
+```json
+"name": "My Awesome App"
+```
+
+### version
+
+**Type:** `string`
+
+Human-readable version string following semantic versioning (MAJOR.MINOR.PATCH).
+
+```json
+"version": "1.0.0"
+"version": "2.1.3-beta"
+```
+
+### version_code
+
+**Type:** `integer`
+
+Numeric version code that must increase with each release. Used to determine if an update is available.
+
+```json
+"version_code": 1
+```
+
+Rules:
+- Must be a positive integer
+- Must be greater than all previously published versions
+- Maximum value: 2147483647
+
+### entry
+
+**Type:** `string`
+
+Path to the main RML file, relative to package root.
+
+```json
+"entry": "assets/main.rml"
+```
+
+### author
+
+**Type:** `object`
+
+Information about the app developer.
+
+```json
+"author": {
+ "name": "Developer Name",
+ "email": "dev@example.com",
+ "url": "https://example.com"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | Yes | Developer or company name |
+| `email` | string | Yes | Contact email |
+| `url` | string | No | Website URL |
+
+### icons
+
+**Type:** `object`
+
+App icons at various sizes. At minimum, provide a 128px icon.
+
+```json
+"icons": {
+ "32": "icons/icon-32.png",
+ "64": "icons/icon-64.png",
+ "128": "icons/icon-128.png"
+}
+```
+
+Supported sizes: 32, 64, 128, 256, 512
+
+Requirements:
+- PNG format recommended
+- Square aspect ratio
+- No transparency on edges (for proper display)
+
+## Optional Fields
+
+### description
+
+**Type:** `string`
+
+Short description shown in app listings. Maximum 200 characters.
+
+```json
+"description": "A simple calculator for everyday math."
+```
+
+### permissions
+
+**Type:** `array`
+
+List of permissions your app requires. Apps cannot access restricted features without declaring permissions.
+
+```json
+"permissions": ["storage", "network"]
+```
+
+See [Permissions](#permissions-reference) below.
+
+### min_mosis_version
+
+**Type:** `string`
+
+Minimum Mosis version required to run this app.
+
+```json
+"min_mosis_version": "1.0.0"
+```
+
+If omitted, defaults to `"1.0.0"`.
+
+### category
+
+**Type:** `string`
+
+App store category for discovery.
+
+```json
+"category": "productivity"
+```
+
+Valid categories:
+- `games`
+- `entertainment`
+- `productivity`
+- `utilities`
+- `social`
+- `communication`
+- `lifestyle`
+- `education`
+- `health`
+- `finance`
+- `news`
+- `other`
+
+### screenshots
+
+**Type:** `array`
+
+Paths to screenshot images for app store listing.
+
+```json
+"screenshots": [
+ "screenshots/home.png",
+ "screenshots/settings.png",
+ "screenshots/detail.png"
+]
+```
+
+Requirements:
+- PNG format
+- 1080x1920 (9:16 portrait) recommended
+- Maximum 5 screenshots
+
+### locales
+
+**Type:** `object`
+
+Internationalization configuration.
+
+```json
+"locales": {
+ "default": "en",
+ "supported": ["en", "es", "fr", "de", "ja"]
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `default` | string | Default locale code |
+| `supported` | array | List of supported locale codes |
+
+Locale files should be placed in `locales/{code}.json`.
+
+## Permissions Reference
+
+| Permission | Description | Example Use |
+|------------|-------------|-------------|
+| `storage` | Persist data locally | Save user preferences |
+| `network` | Make HTTP requests | Fetch remote data |
+| `clipboard` | Read/write clipboard | Copy text |
+| `notifications` | Show notifications | Reminders |
+| `camera` | Access device camera | Photo capture |
+| `location` | Get device location | Maps, weather |
+| `contacts` | Read contacts | Contact picker |
+| `microphone` | Record audio | Voice notes |
+
+### Permission Declaration
+
+```json
+"permissions": [
+ "storage",
+ "network"
+]
+```
+
+Users are informed of permissions before installing. Request only what you need.
+
+## Validation
+
+The package builder validates your manifest. Common errors:
+
+| Error | Cause | Solution |
+|-------|-------|----------|
+| `Invalid package ID` | ID doesn't match pattern | Use `com.company.app` format |
+| `Missing required field` | Required field omitted | Add the field |
+| `Invalid version_code` | Not a positive integer | Use positive number |
+| `Icon not found` | Icon path doesn't exist | Check file paths |
+| `Invalid permission` | Unknown permission | Use valid permission name |
+
+## Example: Minimal Manifest
+
+```json
+{
+ "id": "com.example.hello",
+ "name": "Hello World",
+ "version": "1.0.0",
+ "version_code": 1,
+ "entry": "main.rml",
+ "author": {
+ "name": "Developer",
+ "email": "dev@example.com"
+ },
+ "icons": {
+ "128": "icon.png"
+ }
+}
+```
+
+## Example: Full Manifest
+
+```json
+{
+ "id": "com.acme.calculator",
+ "name": "ACME Calculator",
+ "version": "2.1.0",
+ "version_code": 5,
+ "entry": "assets/main.rml",
+ "description": "A powerful calculator with scientific functions.",
+ "category": "utilities",
+ "permissions": [
+ "storage",
+ "clipboard"
+ ],
+ "min_mosis_version": "1.2.0",
+ "author": {
+ "name": "ACME Corp",
+ "email": "apps@acme.com",
+ "url": "https://acme.com"
+ },
+ "icons": {
+ "32": "icons/icon-32.png",
+ "64": "icons/icon-64.png",
+ "128": "icons/icon-128.png",
+ "256": "icons/icon-256.png"
+ },
+ "screenshots": [
+ "screenshots/basic.png",
+ "screenshots/scientific.png",
+ "screenshots/history.png"
+ ],
+ "locales": {
+ "default": "en",
+ "supported": ["en", "es", "fr", "de"]
+ }
+}
+```
+
+## Next Steps
+
+- [Getting Started](../getting-started.md) - Create your first app
+- [Permissions Guide](../guides/permissions.md) - Understanding permissions
+- [Publishing Guide](../guides/publishing.md) - Submit your app
diff --git a/portal/internal/web/docs/cli.md b/portal/internal/web/docs/cli.md
new file mode 100644
index 0000000..997501a
--- /dev/null
+++ b/portal/internal/web/docs/cli.md
@@ -0,0 +1,576 @@
+# CLI Reference
+
+The Mosis CLI (`mosis`) is a command-line tool for building, testing, and publishing Mosis apps.
+
+## Installation
+
+### Windows
+
+Download the installer from your [Developer Dashboard](/dashboard) or use:
+
+```powershell
+# Using winget
+winget install omixlab.mosis-cli
+
+# Or download directly
+curl -o mosis-cli.exe https://dl.omixlab.com/cli/windows/mosis.exe
+```
+
+### macOS
+
+```bash
+# Using Homebrew
+brew install omixlab/tap/mosis-cli
+
+# Or download directly
+curl -fsSL https://dl.omixlab.com/cli/macos/mosis > /usr/local/bin/mosis
+chmod +x /usr/local/bin/mosis
+```
+
+### Linux
+
+```bash
+# Download binary
+curl -fsSL https://dl.omixlab.com/cli/linux/mosis > ~/.local/bin/mosis
+chmod +x ~/.local/bin/mosis
+
+# Or using snap
+sudo snap install mosis-cli
+```
+
+## Quick Start
+
+```bash
+# Create a new project
+mosis init myapp
+
+# Build the package
+cd myapp
+mosis build
+
+# Test locally
+mosis run
+
+# Login and publish
+mosis login
+mosis publish
+```
+
+## Commands
+
+### mosis init
+
+Create a new Mosis app project.
+
+```bash
+mosis init [options]
+```
+
+**Arguments:**
+- `name` - Project name (creates directory)
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--template ` | Use a starter template |
+| `--package-id ` | Set package ID |
+| `--no-git` | Don't initialize git repo |
+
+**Templates:**
+- `default` - Basic app structure
+- `minimal` - Bare minimum files
+- `navigation` - Multi-screen with navigation
+- `form` - Form handling example
+- `list` - Scrollable list example
+
+**Example:**
+```bash
+mosis init myapp --template navigation --package-id com.example.myapp
+```
+
+**Output:**
+```
+myapp/
+├── manifest.json
+├── icon.png
+├── assets/
+│ ├── main.rml
+│ └── styles.rcss
+└── .mosis/
+ └── config.json
+```
+
+---
+
+### mosis build
+
+Build a `.mosis` package from your project.
+
+```bash
+mosis build [options]
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `-o, --output ` | Output file path |
+| `--no-sign` | Skip signing (dev only) |
+| `--verbose` | Show detailed output |
+| `--validate-only` | Validate without building |
+
+**Example:**
+```bash
+mosis build -o dist/myapp.mosis
+```
+
+**Build Process:**
+1. Validates manifest.json
+2. Checks all referenced files exist
+3. Validates RML/RCSS syntax
+4. Creates compressed package
+5. Signs with developer key (if available)
+
+---
+
+### mosis validate
+
+Validate your project without building.
+
+```bash
+mosis validate [options]
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--strict` | Enable strict validation |
+| `--fix` | Auto-fix simple issues |
+
+**Checks performed:**
+- Manifest schema validation
+- Required files existence
+- Icon sizes and formats
+- RML/RCSS syntax
+- Lua syntax
+- Package size limits
+
+**Example output:**
+```
+✓ manifest.json is valid
+✓ All required icons present
+✓ Entry point exists: assets/main.rml
+✓ RML syntax valid (3 files)
+✓ RCSS syntax valid (2 files)
+✓ Lua syntax valid (1 file)
+✓ Package size: 45KB (under 10MB limit)
+
+Validation passed!
+```
+
+---
+
+### mosis run
+
+Run your app in the local designer for testing.
+
+```bash
+mosis run [options]
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--device ` | Target device profile |
+| `--scale ` | Window scale factor |
+| `--hot-reload` | Enable hot reload (default) |
+| `--no-hot-reload` | Disable hot reload |
+
+**Device profiles:**
+- `phone` - Standard phone (1080x1920)
+- `tablet` - Tablet (1200x1920)
+- `watch` - Watch (360x360)
+
+**Example:**
+```bash
+mosis run --device phone --scale 0.5
+```
+
+---
+
+### mosis login
+
+Authenticate with the developer portal.
+
+```bash
+mosis login [options]
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--token ` | Use API token directly |
+| `--browser` | Open browser for OAuth |
+
+**Interactive login:**
+```bash
+$ mosis login
+Opening browser for authentication...
+✓ Logged in as developer@example.com
+```
+
+**Token login (for CI/CD):**
+```bash
+mosis login --token YOUR_API_TOKEN
+```
+
+---
+
+### mosis logout
+
+Log out of the developer portal.
+
+```bash
+mosis logout
+```
+
+---
+
+### mosis publish
+
+Upload and submit your app for review.
+
+```bash
+mosis publish [options]
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--notes ` | Release notes |
+| `--notes-file ` | Release notes from file |
+| `--draft` | Upload as draft (don't submit) |
+| `--track ` | Release track (production/beta) |
+
+**Example:**
+```bash
+mosis publish --notes "Bug fixes and performance improvements"
+```
+
+**Process:**
+1. Builds package (if needed)
+2. Uploads to portal
+3. Runs automated validation
+4. Submits for review (unless `--draft`)
+
+---
+
+### mosis status
+
+Check the status of your app submissions.
+
+```bash
+mosis status [app-id]
+```
+
+**Example output:**
+```
+com.example.myapp
+
+Latest Version: 1.2.0 (code: 5)
+Status: In Review
+Submitted: 2 hours ago
+
+Previous Versions:
+ 1.1.0 (4) - Published
+ 1.0.0 (1) - Published
+```
+
+---
+
+### mosis keys
+
+Manage signing keys.
+
+```bash
+mosis keys
+```
+
+**Subcommands:**
+
+#### keys generate
+
+Generate a new signing keypair.
+
+```bash
+mosis keys generate [options]
+```
+
+| Option | Description |
+|--------|-------------|
+| `-o, --output ` | Output directory |
+| `--name ` | Key name |
+
+```bash
+$ mosis keys generate --name production
+Generated keypair:
+ Private: ~/.mosis/keys/production.key
+ Public: ~/.mosis/keys/production.pub
+
+Keep your private key secure! Never share it.
+```
+
+#### keys register
+
+Upload your public key to the portal.
+
+```bash
+mosis keys register
+```
+
+```bash
+$ mosis keys register ~/.mosis/keys/production.pub
+✓ Key registered successfully
+ Key ID: k_abc123xyz
+ Algorithm: Ed25519
+```
+
+#### keys list
+
+List registered keys.
+
+```bash
+$ mosis keys list
+ID Name Created Status
+k_abc123xyz production 2024-01-15 Active
+k_def456uvw development 2024-01-10 Active
+```
+
+#### keys revoke
+
+Revoke a registered key.
+
+```bash
+mosis keys revoke
+```
+
+---
+
+### mosis config
+
+Manage CLI configuration.
+
+```bash
+mosis config
+```
+
+**Subcommands:**
+
+#### config get
+
+Get a configuration value.
+
+```bash
+mosis config get
+```
+
+#### config set
+
+Set a configuration value.
+
+```bash
+mosis config set
+```
+
+#### config list
+
+List all configuration.
+
+```bash
+$ mosis config list
+api_url = https://api.omixlab.com
+designer_path = /usr/local/bin/mosis-designer
+default_key = production
+```
+
+**Configuration keys:**
+| Key | Description | Default |
+|-----|-------------|---------|
+| `api_url` | API endpoint | https://api.omixlab.com |
+| `designer_path` | Path to designer | (auto-detected) |
+| `default_key` | Default signing key | (none) |
+| `auto_build` | Build before publish | true |
+
+---
+
+### mosis doctor
+
+Diagnose common issues with your setup.
+
+```bash
+$ mosis doctor
+Checking Mosis CLI installation...
+
+✓ CLI version: 1.2.0
+✓ Designer found: /usr/local/bin/mosis-designer
+✓ Authenticated as: developer@example.com
+✓ Signing key configured: production
+✓ Network connectivity OK
+
+All checks passed!
+```
+
+---
+
+### mosis version
+
+Show CLI version information.
+
+```bash
+$ mosis version
+mosis-cli version 1.2.0
+Built: 2024-01-15
+Go: 1.21.5
+```
+
+---
+
+### mosis help
+
+Show help for any command.
+
+```bash
+mosis help [command]
+mosis --help
+```
+
+## Environment Variables
+
+| Variable | Description |
+|----------|-------------|
+| `MOSIS_API_URL` | Override API endpoint |
+| `MOSIS_TOKEN` | API token for authentication |
+| `MOSIS_KEY_PATH` | Path to signing key |
+| `MOSIS_NO_COLOR` | Disable colored output |
+| `MOSIS_DEBUG` | Enable debug logging |
+
+## Configuration Files
+
+### Global Config
+
+Location: `~/.mosis/config.json`
+
+```json
+{
+ "api_url": "https://api.omixlab.com",
+ "default_key": "production",
+ "auto_build": true
+}
+```
+
+### Project Config
+
+Location: `.mosis/config.json` (in project root)
+
+```json
+{
+ "signing_key": "production",
+ "build_output": "dist/"
+}
+```
+
+## Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| 0 | Success |
+| 1 | General error |
+| 2 | Invalid arguments |
+| 3 | Authentication required |
+| 4 | Validation failed |
+| 5 | Network error |
+| 6 | Build failed |
+
+## CI/CD Integration
+
+### GitHub Actions
+
+```yaml
+name: Publish Mosis App
+on:
+ push:
+ tags: ['v*']
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Mosis CLI
+ run: |
+ curl -fsSL https://dl.omixlab.com/cli/linux/mosis > mosis
+ chmod +x mosis
+ sudo mv mosis /usr/local/bin/
+
+ - name: Build and Publish
+ env:
+ MOSIS_TOKEN: ${{ secrets.MOSIS_TOKEN }}
+ run: |
+ mosis build
+ mosis publish --notes "Release ${GITHUB_REF#refs/tags/}"
+```
+
+### GitLab CI
+
+```yaml
+publish:
+ image: ubuntu:latest
+ script:
+ - curl -fsSL https://dl.omixlab.com/cli/linux/mosis > /usr/local/bin/mosis
+ - chmod +x /usr/local/bin/mosis
+ - mosis build
+ - mosis publish --notes "Release $CI_COMMIT_TAG"
+ only:
+ - tags
+ variables:
+ MOSIS_TOKEN: $MOSIS_TOKEN
+```
+
+## Troubleshooting
+
+### "Command not found"
+
+Ensure the CLI is in your PATH:
+```bash
+echo $PATH
+which mosis
+```
+
+### "Authentication failed"
+
+Re-login:
+```bash
+mosis logout
+mosis login
+```
+
+### "Build failed: Invalid manifest"
+
+Run validation for details:
+```bash
+mosis validate --strict
+```
+
+### "Network error"
+
+Check connectivity:
+```bash
+mosis doctor
+curl -I https://api.omixlab.com/health
+```
+
+## See Also
+
+- [Getting Started](getting-started.md) - First app tutorial
+- [Publishing Guide](guides/publishing.md) - Submission tips
+- [API Reference](api/lua-api.md) - Lua API documentation
diff --git a/portal/internal/web/docs/faq.md b/portal/internal/web/docs/faq.md
new file mode 100644
index 0000000..f60a185
--- /dev/null
+++ b/portal/internal/web/docs/faq.md
@@ -0,0 +1,267 @@
+# Frequently Asked Questions
+
+## General
+
+### What is Mosis?
+
+Mosis is a virtual smartphone OS for VR games and applications. It provides a phone-like device that users can interact with inside VR environments, complete with real smartphone functionality.
+
+### Who can develop apps for Mosis?
+
+Anyone! Sign up for a free developer account to start building apps. There's no fee to register or submit apps.
+
+### What can I build with Mosis?
+
+You can build any app that works on a phone screen:
+- Utilities (calculators, converters, timers)
+- Productivity (notes, to-do lists, calendars)
+- Games (puzzles, casual games)
+- Entertainment (media players, readers)
+- Social apps (chat, messaging)
+- And more!
+
+### How do users get my app?
+
+Users discover and install apps through the Mosis App Store, which is built into the virtual phone. Published apps appear in store listings where users can browse, search, and install.
+
+## Development
+
+### What languages/technologies do I need to know?
+
+Mosis apps use:
+- **RML** - Similar to HTML for structure
+- **RCSS** - Similar to CSS for styling
+- **Lua** - Lightweight scripting language for logic
+
+If you know HTML/CSS, you'll find RML/RCSS very familiar. Lua is simple to learn and has many tutorials available.
+
+### Can I use JavaScript instead of Lua?
+
+No, Mosis uses Lua for scripting. Lua was chosen for its:
+- Lightweight footprint
+- Easy sandboxing for security
+- Simple learning curve
+- Fast execution
+
+### Can I use React/Vue/Angular?
+
+No, Mosis uses its own RML/RCSS system based on RmlUi. Standard web frameworks won't work, but the concepts are similar enough that web developers can adapt quickly.
+
+### What IDEs are supported?
+
+Use any text editor! VS Code is recommended with these extensions:
+- Lua Language Server
+- XML/HTML tools for RML editing
+
+### Is there a visual designer?
+
+The Desktop Designer provides:
+- Live preview of your app
+- Hot reload on file changes
+- Hierarchy inspection
+- Screenshot capture
+
+It's included in your developer tools download.
+
+### Can I test on a real device?
+
+Yes! You can:
+1. Install the Designer on your PC
+2. Build a .mosis package
+3. Sideload onto a VR device with MosisService installed
+
+### How large can my app be?
+
+The maximum package size is **10MB**. This is plenty for most apps. If you need more:
+- Optimize images (use TGA format)
+- Remove unused assets
+- Load large data from the network
+
+### Can I use external APIs?
+
+Yes, with the `network` permission. Make HTTPS requests to any API:
+
+```lua
+http.get("https://api.example.com/data", function(response)
+ local data = json.decode(response.body)
+end)
+```
+
+### Can I access the device camera/microphone?
+
+Yes, with the appropriate permissions:
+- `camera` - For photo capture
+- `microphone` - For audio recording
+
+Users will be prompted to grant access.
+
+### Can my app run in the background?
+
+Currently, apps only run when visible. Background execution is planned for future versions.
+
+### Can I access native device features?
+
+Mosis apps are sandboxed for security. Available device features:
+- Storage
+- Network
+- Camera (with permission)
+- Microphone (with permission)
+- Clipboard
+- Notifications
+
+Direct hardware access (Bluetooth, USB, etc.) is not available.
+
+## Publishing
+
+### How long does review take?
+
+Most apps are reviewed within 24-48 hours. Apps requesting sensitive permissions may take longer.
+
+Automated checks run instantly. Manual review is triggered for:
+- First-time developers
+- Sensitive permissions
+- Flagged content
+
+### Why was my app rejected?
+
+Check the rejection reason in your dashboard. Common reasons:
+- Crashes on launch
+- Missing required assets
+- Policy violations
+- Inappropriate content
+- Misleading metadata
+
+### Can I update my app?
+
+Yes! Submit a new version with:
+- Higher `version_code`
+- Updated `version` string
+- Release notes
+
+Updates go through the same review process.
+
+### Can I remove my app from the store?
+
+Yes, go to your app's settings and choose "Unpublish". Users who installed it can keep using it, but it won't appear in searches.
+
+### Can I have paid apps?
+
+Currently, all apps are free. Paid apps and in-app purchases are planned for future versions.
+
+### What's the revenue share?
+
+When monetization launches, the split will be:
+- **70%** to developers
+- **30%** to Mosis platform
+
+### Can I distribute outside the store?
+
+Yes, you can share `.mosis` files directly. However:
+- Users must enable sideloading
+- Updates won't be automatic
+- No store discoverability
+
+## Technical
+
+### What RML/RCSS version is supported?
+
+Mosis uses RmlUi 6.x. The [UI Design Guide](guides/ui-design.md) covers supported features. Not all CSS3 features are available.
+
+### What Lua version is supported?
+
+Lua 5.4 with some restrictions for sandboxing. See the [Lua Scripting Guide](guides/lua-scripting.md) for details.
+
+### Are there size limits for storage?
+
+Each app has 5MB of local storage. For more data, use network storage.
+
+### Can I use databases?
+
+Use the `storage` API for key-value storage. SQLite is not directly available, but you can:
+- Store JSON data
+- Use a remote database via network
+
+### How do I handle different screen sizes?
+
+Design for the standard phone screen (1080x1920 logical pixels). Use:
+- `dp` units for consistent sizing
+- Flexbox for flexible layouts
+- Percentage widths for adaptability
+
+### Can I create multiple screens?
+
+Yes, use the navigation system:
+
+```lua
+navigateTo("settings") -- Load settings.rml
+goBack() -- Return to previous screen
+```
+
+### Can apps communicate with each other?
+
+Currently, apps are isolated. Inter-app communication is planned for future versions.
+
+### What happens if my app crashes?
+
+Crashes are caught by the sandbox. The user sees an error message and can restart. Crash reports are sent to your analytics dashboard (if telemetry is enabled).
+
+### Can I access the file system?
+
+No direct file system access. Use:
+- `storage` API for persisted data
+- Bundled assets for static files
+- `http` API for remote files
+
+## Account & Legal
+
+### Is there a developer fee?
+
+No, developer accounts are free. There's no cost to register, develop, or publish apps.
+
+### Can I transfer my app to another developer?
+
+Contact support to request a transfer.
+
+### What content is not allowed?
+
+- Malware or security exploits
+- Hate speech or discrimination
+- Adult content (unless properly rated)
+- Copyright infringement
+- Privacy violations
+- Impersonation of other apps/brands
+
+See the full content policy in your developer agreement.
+
+### Do I need a privacy policy?
+
+You need a privacy policy if your app:
+- Collects user data
+- Uses analytics
+- Makes network requests
+- Accesses contacts, location, etc.
+
+### Who owns the IP for my app?
+
+You retain all intellectual property rights to your app. By publishing on Mosis, you grant a license to distribute it through the store.
+
+### Can I use open source code?
+
+Yes, but respect the licenses:
+- MIT, BSD, Apache: Generally safe
+- GPL: May require source distribution
+- Proprietary: Check terms carefully
+
+## More Questions?
+
+If your question isn't answered here:
+
+1. Check the [Troubleshooting](troubleshooting.md) guide
+2. Search the developer forum
+3. Contact support through your dashboard
+
+## See Also
+
+- [Getting Started](getting-started.md) - Create your first app
+- [Troubleshooting](troubleshooting.md) - Common problems and solutions
+- [API Reference](api/lua-api.md) - Complete API documentation
diff --git a/portal/internal/web/docs/getting-started.md b/portal/internal/web/docs/getting-started.md
new file mode 100644
index 0000000..0090d9f
--- /dev/null
+++ b/portal/internal/web/docs/getting-started.md
@@ -0,0 +1,190 @@
+# Getting Started
+
+This guide walks you through creating your first Mosis app in under 10 minutes.
+
+## Prerequisites
+
+- A Mosis developer account ([sign up here](/register))
+- Text editor (VS Code recommended)
+- Desktop Designer for testing (download from portal)
+
+## Step 1: Create a New App
+
+1. Log in to the [Developer Portal](/dashboard)
+2. Click **Create New App**
+3. Fill in the details:
+ - **Package ID**: `com.yourname.myapp` (unique identifier)
+ - **App Name**: My First App
+ - **Description**: A simple hello world app
+4. Click **Create**
+
+## Step 2: Set Up Your Project
+
+Create a project folder with this structure:
+
+```
+myapp/
+├── manifest.json
+├── icon.png
+└── assets/
+ ├── main.rml
+ └── styles.rcss
+```
+
+### manifest.json
+
+```json
+{
+ "id": "com.yourname.myapp",
+ "name": "My First App",
+ "version": "1.0.0",
+ "version_code": 1,
+ "entry": "assets/main.rml",
+ "permissions": [],
+ "min_mosis_version": "1.0.0",
+ "author": {
+ "name": "Your Name",
+ "email": "you@example.com"
+ },
+ "icons": {
+ "32": "icon.png",
+ "64": "icon.png",
+ "128": "icon.png"
+ }
+}
+```
+
+### assets/main.rml
+
+```xml
+
+
+ My First App
+
+
+
+
+
Hello, Mosis!
+
This is my first app.
+
+
Clicks: 0
+
+
+
+
+
+```
+
+### assets/styles.rcss
+
+```css
+body {
+ font-family: LatoLatin;
+ background-color: #1a1a2e;
+ color: #ffffff;
+}
+
+.container {
+ padding: 20dp;
+ text-align: center;
+}
+
+h1 {
+ font-size: 24dp;
+ margin-bottom: 10dp;
+ color: #00d4ff;
+}
+
+p {
+ font-size: 16dp;
+ margin-bottom: 20dp;
+}
+
+button {
+ background-color: #00d4ff;
+ color: #1a1a2e;
+ padding: 12dp 24dp;
+ border-radius: 8dp;
+ font-size: 16dp;
+ font-weight: bold;
+}
+
+button:hover {
+ background-color: #00b8e6;
+}
+
+button:active {
+ background-color: #0099cc;
+}
+
+#counter {
+ font-size: 18dp;
+ margin-top: 20dp;
+}
+```
+
+### icon.png
+
+Create a 128x128 PNG icon for your app. Use any image editor or find a placeholder icon.
+
+## Step 3: Test Locally
+
+1. Download and install the Desktop Designer from your dashboard
+2. Open a terminal in your project folder
+3. Run:
+
+```bash
+mosis-designer.exe assets/main.rml
+```
+
+The designer window opens showing your app. Changes to RML, RCSS, or Lua files automatically reload.
+
+## Step 4: Build Your Package
+
+From your project folder:
+
+```bash
+mosis build
+```
+
+This creates `myapp.mosis` - your packaged app ready for submission.
+
+## Step 5: Submit for Review
+
+1. Go to your app in the Developer Portal
+2. Click **Create New Version**
+3. Upload your `.mosis` package
+4. Add release notes
+5. Click **Submit for Review**
+
+Your app will be reviewed automatically. If it passes all checks, you can publish it to the store.
+
+## Next Steps
+
+- [UI Design Guide](guides/ui-design.md) - Learn RML/RCSS in depth
+- [Lua Scripting Guide](guides/lua-scripting.md) - Add complex interactivity
+- [Permissions Guide](guides/permissions.md) - Request device capabilities
+- [Publishing Guide](guides/publishing.md) - Tips for successful submissions
+
+## Example Apps
+
+Check out these example apps to learn from:
+
+| App | Description | Source |
+|-----|-------------|--------|
+| Calculator | Basic calculator | [View](examples/calculator.md) |
+| Notes | Simple note-taking | [View](examples/notes.md) |
+| Timer | Countdown timer | [View](examples/timer.md) |
+
+## Getting Help
+
+- Join our [Discord community](#)
+- Check the [FAQ](faq.md)
+- Search the [Troubleshooting guide](troubleshooting.md)
diff --git a/portal/internal/web/docs/guides/best-practices.md b/portal/internal/web/docs/guides/best-practices.md
new file mode 100644
index 0000000..ad1891d
--- /dev/null
+++ b/portal/internal/web/docs/guides/best-practices.md
@@ -0,0 +1,535 @@
+# Best Practices
+
+Guidelines for building high-quality Mosis apps that users love.
+
+## Performance
+
+### Minimize DOM Queries
+
+Cache element references instead of querying repeatedly:
+
+```lua
+-- Bad: Queries on every frame
+function updateScore()
+ document:GetElementById("score").inner_rml = tostring(score)
+end
+
+-- Good: Cache the reference
+local scoreElement
+
+function onLoad()
+ scoreElement = document:GetElementById("score")
+end
+
+function updateScore()
+ scoreElement.inner_rml = tostring(score)
+end
+```
+
+### Batch DOM Updates
+
+Group multiple changes together:
+
+```lua
+-- Bad: Multiple separate updates
+elem1.style.color = "red"
+elem2.style.color = "red"
+elem3.style.color = "red"
+
+-- Good: Use a class
+parent:SetClass("error-state", true)
+```
+
+### Use Efficient Data Structures
+
+```lua
+-- For frequent lookups, use tables as maps
+local itemLookup = {}
+for i, item in ipairs(items) do
+ itemLookup[item.id] = item
+end
+
+-- O(1) lookup instead of O(n) search
+local item = itemLookup["item-123"]
+```
+
+### Clean Up Timers
+
+Always clear intervals when navigating away:
+
+```lua
+local updateInterval
+
+function onScreenLoad()
+ updateInterval = setInterval(function()
+ updateData()
+ end, 1000)
+end
+
+function onScreenUnload()
+ if updateInterval then
+ clearInterval(updateInterval)
+ updateInterval = nil
+ end
+end
+```
+
+### Lazy Load Content
+
+Don't load everything at startup:
+
+```lua
+-- Load data when user scrolls to section
+function onSectionVisible(sectionId)
+ if not loadedSections[sectionId] then
+ loadSectionData(sectionId)
+ loadedSections[sectionId] = true
+ end
+end
+```
+
+## User Experience
+
+### Provide Feedback
+
+Show users that actions are happening:
+
+```lua
+function onSubmit()
+ -- Show loading state immediately
+ submitButton:SetClass("loading", true)
+ submitButton:SetAttribute("disabled", "disabled")
+
+ http.post(url, data, function(response)
+ submitButton:SetClass("loading", false)
+ submitButton:RemoveAttribute("disabled")
+
+ if response.ok then
+ showSuccess("Saved!")
+ else
+ showError("Failed to save")
+ end
+ end)
+end
+```
+
+### Handle Errors Gracefully
+
+Never show raw error messages to users:
+
+```lua
+http.get(url, function(response)
+ if response.ok then
+ displayData(json.decode(response.body))
+ else
+ -- User-friendly message
+ showMessage("Unable to load data. Please check your connection.")
+
+ -- Log details for debugging
+ console.error("API error:", response.status, response.body)
+ end
+end)
+```
+
+### Make Touch Targets Large Enough
+
+Minimum 48dp for touchable elements:
+
+```css
+.button {
+ min-width: 48dp;
+ min-height: 48dp;
+ padding: 12dp 24dp;
+}
+
+.list-item {
+ min-height: 56dp;
+ padding: 16dp;
+}
+```
+
+### Support Undo for Destructive Actions
+
+```lua
+local deletedItem = nil
+local undoTimeout = nil
+
+function deleteItem(itemId)
+ deletedItem = items[itemId]
+ items[itemId] = nil
+ updateList()
+
+ showUndoSnackbar("Item deleted", function()
+ -- Undo callback
+ items[itemId] = deletedItem
+ deletedItem = nil
+ updateList()
+ end)
+
+ -- Clear undo after 5 seconds
+ undoTimeout = setTimeout(function()
+ deletedItem = nil
+ permanentlyDelete(itemId)
+ end, 5000)
+end
+```
+
+### Remember User State
+
+Restore position and selections when returning:
+
+```lua
+function onScreenUnload()
+ storage.set("list_scroll_position", scrollContainer.scroll_top)
+ storage.set("selected_tab", currentTab)
+end
+
+function onScreenLoad()
+ local scrollPos = storage.get("list_scroll_position")
+ if scrollPos then
+ scrollContainer.scroll_top = scrollPos
+ end
+
+ local tab = storage.get("selected_tab")
+ if tab then
+ selectTab(tab)
+ end
+end
+```
+
+## Code Quality
+
+### Use Local Variables
+
+Local variables are faster and prevent global pollution:
+
+```lua
+-- Bad: Global
+count = 0
+
+-- Good: Local
+local count = 0
+
+-- Good: Module-level local
+local Utils = {}
+local cache = {} -- Private to module
+```
+
+### Handle Edge Cases
+
+```lua
+function divide(a, b)
+ if b == 0 then
+ console.warn("Division by zero")
+ return 0
+ end
+ return a / b
+end
+
+function getUsername(user)
+ if not user then
+ return "Unknown"
+ end
+ return user.name or user.email or "Unknown"
+end
+```
+
+### Use Meaningful Names
+
+```lua
+-- Bad
+local t = {}
+local n = 0
+
+-- Good
+local userScores = {}
+local attemptCount = 0
+
+-- Bad
+function p(x)
+ return x * 100
+end
+
+-- Good
+function toPercentage(decimal)
+ return decimal * 100
+end
+```
+
+### Keep Functions Small
+
+Each function should do one thing:
+
+```lua
+-- Bad: Does too much
+function processUser(userId)
+ local user = fetchUser(userId)
+ validateUser(user)
+ updateUserStats(user)
+ sendWelcomeEmail(user)
+ logActivity(user)
+ return formatUserResponse(user)
+end
+
+-- Good: Composed of small functions
+function processNewUser(userId)
+ local user = fetchUser(userId)
+ if not isValidUser(user) then
+ return nil, "Invalid user"
+ end
+ initializeUserStats(user)
+ queueWelcomeEmail(user)
+ return user
+end
+```
+
+### Comment Why, Not What
+
+```lua
+-- Bad: Describes what (obvious from code)
+-- Increment counter by 1
+counter = counter + 1
+
+-- Good: Explains why
+-- Reset retry count after successful connection
+-- to prevent unnecessary backoff on next attempt
+retryCount = 0
+```
+
+## Security
+
+### Validate All Input
+
+```lua
+function searchItems(query)
+ -- Sanitize input
+ if type(query) ~= "string" then
+ return {}
+ end
+
+ query = query:sub(1, 100) -- Limit length
+ query = query:gsub("[^%w%s]", "") -- Remove special chars
+
+ return performSearch(query)
+end
+```
+
+### Don't Trust External Data
+
+```lua
+http.get(url, function(response)
+ local success, data = pcall(function()
+ return json.decode(response.body)
+ end)
+
+ if not success then
+ console.error("Invalid JSON from API")
+ return
+ end
+
+ -- Validate structure
+ if type(data.items) ~= "table" then
+ console.error("Missing items array")
+ return
+ end
+
+ processItems(data.items)
+end)
+```
+
+### Never Store Secrets in Code
+
+```lua
+-- Bad: Hardcoded API key
+local API_KEY = "sk-12345abcde"
+
+-- Good: Use environment/config
+local apiKey = config.get("api_key")
+```
+
+### Sanitize Display Content
+
+When displaying user-generated content, prevent injection:
+
+```lua
+function displayComment(text)
+ -- Escape HTML entities
+ text = text:gsub("&", "&")
+ text = text:gsub("<", "<")
+ text = text:gsub(">", ">")
+
+ commentElement.inner_rml = text
+end
+```
+
+## Accessibility
+
+### Use Semantic Elements
+
+```xml
+
+
Submit
+
+
+
+
+
+
Settings
+
Account
+
Notifications
+```
+
+### Provide Text Alternatives
+
+```xml
+
+
+
+
+
+```
+
+### Ensure Color Contrast
+
+Text should have at least 4.5:1 contrast ratio:
+
+```css
+/* Good contrast */
+.light-text {
+ color: #ffffff;
+ background-color: #1a1a2e; /* Contrast: 12.6:1 */
+}
+
+/* Bad contrast */
+.low-contrast {
+ color: #888888;
+ background-color: #666666; /* Contrast: 1.3:1 */
+}
+```
+
+### Don't Rely on Color Alone
+
+```css
+/* Bad: Only color indicates error */
+.error {
+ color: red;
+}
+
+/* Good: Icon + color + text */
+.error {
+ color: #ff4444;
+}
+.error::before {
+ content: "⚠ ";
+}
+```
+
+## Testing
+
+### Test Error States
+
+Don't just test the happy path:
+
+```lua
+-- Test these scenarios:
+-- 1. Empty data
+-- 2. Network failure
+-- 3. Invalid input
+-- 4. Timeouts
+-- 5. Missing permissions
+```
+
+### Test Navigation Flows
+
+Ensure users can:
+- Navigate forward and back
+- Return to the home screen
+- Handle the back button at any screen
+
+### Test Edge Cases
+
+- Very long text/names
+- Empty lists
+- Maximum values
+- Rapid repeated actions
+- Interrupted operations
+
+### Use Debug Logging
+
+```lua
+local DEBUG = true
+
+function debugLog(...)
+ if DEBUG then
+ print("[DEBUG]", ...)
+ end
+end
+
+-- In production build, set DEBUG = false
+```
+
+## Deployment
+
+### Use Meaningful Version Numbers
+
+Follow semantic versioning:
+- **MAJOR**: Breaking changes
+- **MINOR**: New features, backward compatible
+- **PATCH**: Bug fixes
+
+```json
+{
+ "version": "2.1.3",
+ "version_code": 15
+}
+```
+
+### Write Good Release Notes
+
+```
+Version 2.1.0
+
+New Features:
+- Added dark mode support
+- New export to PDF feature
+
+Improvements:
+- Faster loading times
+- Better error messages
+
+Bug Fixes:
+- Fixed crash when opening empty files
+- Fixed date format on some devices
+```
+
+### Test Before Submitting
+
+1. Run on the Designer
+2. Test all features manually
+3. Check on a real device if possible
+4. Verify all assets load correctly
+5. Test offline behavior
+
+## Summary Checklist
+
+Before submitting your app:
+
+- [ ] All features work as expected
+- [ ] Error states are handled gracefully
+- [ ] Loading states shown during async operations
+- [ ] Touch targets are at least 48dp
+- [ ] Text is readable (contrast ratio ≥ 4.5:1)
+- [ ] No console errors in normal usage
+- [ ] Timers and intervals cleaned up properly
+- [ ] User data persists correctly
+- [ ] App works after fresh install
+- [ ] Version number and code are updated
+- [ ] Release notes are meaningful
+
+## See Also
+
+- [UI Design Guide](ui-design.md) - Design patterns
+- [Lua Scripting Guide](lua-scripting.md) - Code patterns
+- [Troubleshooting](../troubleshooting.md) - Common issues
diff --git a/portal/internal/web/docs/guides/lua-scripting.md b/portal/internal/web/docs/guides/lua-scripting.md
new file mode 100644
index 0000000..289ff11
--- /dev/null
+++ b/portal/internal/web/docs/guides/lua-scripting.md
@@ -0,0 +1,506 @@
+# Lua Scripting Guide
+
+Mosis apps use Lua for scripting and interactivity. Each app runs in an isolated sandbox with access to Mosis-specific APIs.
+
+## Getting Started
+
+Embed Lua directly in your RML files:
+
+```xml
+
+
+
+
+
+```
+
+Or use external files:
+
+```xml
+
+
+
+```
+
+## Lua Basics
+
+If you're new to Lua, here's a quick primer:
+
+### Variables
+
+```lua
+-- Local variables (preferred)
+local name = "Mosis"
+local count = 42
+local enabled = true
+local items = {"apple", "banana", "cherry"}
+
+-- Global variables (avoid when possible)
+globalVar = "accessible everywhere"
+```
+
+### Functions
+
+```lua
+-- Basic function
+function greet(name)
+ return "Hello, " .. name .. "!"
+end
+
+-- Function with multiple returns
+function getPosition()
+ return 100, 200
+end
+
+local x, y = getPosition()
+
+-- Anonymous functions
+local double = function(n) return n * 2 end
+```
+
+### Control Flow
+
+```lua
+-- If statements
+if score > 100 then
+ print("High score!")
+elseif score > 50 then
+ print("Good job!")
+else
+ print("Keep trying!")
+end
+
+-- Loops
+for i = 1, 10 do
+ print(i)
+end
+
+for index, value in ipairs(items) do
+ print(index, value)
+end
+
+while condition do
+ -- loop body
+end
+```
+
+### Tables
+
+```lua
+-- Array-like table
+local colors = {"red", "green", "blue"}
+print(colors[1]) -- "red" (Lua is 1-indexed)
+
+-- Dictionary-like table
+local user = {
+ name = "Alice",
+ age = 25,
+ premium = true
+}
+print(user.name)
+print(user["age"])
+
+-- Mixed table
+local app = {
+ name = "MyApp",
+ version = "1.0",
+ features = {"dark mode", "notifications"}
+}
+```
+
+## DOM Manipulation
+
+Access and modify UI elements using the `document` object:
+
+### Getting Elements
+
+```lua
+-- By ID
+local button = document:GetElementById("my-button")
+
+-- By tag name
+local paragraphs = document:GetElementsByTagName("p")
+
+-- By class name
+local cards = document:GetElementsByClassName("card")
+```
+
+### Modifying Content
+
+```lua
+local element = document:GetElementById("message")
+
+-- Set inner content (HTML-like)
+element.inner_rml = "Hello!"
+
+-- Get inner content
+local content = element.inner_rml
+
+-- Set text only (safer, no HTML parsing)
+element:SetInnerRML("Plain text here")
+```
+
+### Modifying Attributes
+
+```lua
+local input = document:GetElementById("username")
+
+-- Get attribute
+local value = input:GetAttribute("value")
+
+-- Set attribute
+input:SetAttribute("placeholder", "Enter username")
+
+-- Remove attribute
+input:RemoveAttribute("disabled")
+```
+
+### Modifying Styles
+
+```lua
+local box = document:GetElementById("box")
+
+-- Set individual properties
+box.style.width = "200dp"
+box.style.backgroundColor = "#00d4ff"
+box.style.display = "none" -- hide element
+
+-- Read properties
+local width = box.style.width
+```
+
+### Classes
+
+```lua
+local element = document:GetElementById("panel")
+
+-- Add class
+element:SetClass("active", true)
+
+-- Remove class
+element:SetClass("active", false)
+
+-- Check class
+if element:IsClassSet("active") then
+ print("Panel is active")
+end
+```
+
+## Event Handling
+
+### Inline Events
+
+```xml
+
+
+
+```
+
+### Event Listeners
+
+```lua
+local button = document:GetElementById("my-button")
+
+-- Add listener
+button:AddEventListener("click", function(event)
+ print("Button clicked!")
+end)
+
+-- Remove listener (need reference)
+local handler = function(event)
+ print("Clicked")
+end
+button:AddEventListener("click", handler)
+button:RemoveEventListener("click", handler)
+```
+
+### Event Object
+
+```lua
+function handleEvent(event)
+ -- Event type
+ print(event.type) -- "click", "change", etc.
+
+ -- Target element
+ local target = event:GetCurrentElement()
+
+ -- Mouse position (for mouse events)
+ local x = event.parameters.mouse_x
+ local y = event.parameters.mouse_y
+
+ -- Stop propagation
+ event:StopPropagation()
+end
+```
+
+### Common Events
+
+| Event | Description |
+|-------|-------------|
+| `click` | Element clicked |
+| `dblclick` | Element double-clicked |
+| `mousedown` | Mouse button pressed |
+| `mouseup` | Mouse button released |
+| `mouseover` | Mouse enters element |
+| `mouseout` | Mouse leaves element |
+| `focus` | Element gains focus |
+| `blur` | Element loses focus |
+| `change` | Input value changed |
+| `submit` | Form submitted |
+| `keydown` | Key pressed |
+| `keyup` | Key released |
+
+## Timers
+
+### setTimeout
+
+```lua
+-- Execute once after delay
+local timerId = setTimeout(function()
+ print("Executed after 1 second")
+end, 1000) -- milliseconds
+
+-- Cancel timer
+clearTimeout(timerId)
+```
+
+### setInterval
+
+```lua
+-- Execute repeatedly
+local intervalId = setInterval(function()
+ print("Tick")
+end, 1000)
+
+-- Cancel interval
+clearInterval(intervalId)
+```
+
+## Storage
+
+Persist data between app sessions:
+
+```lua
+-- Save data
+storage.set("username", "Alice")
+storage.set("settings", {
+ darkMode = true,
+ notifications = false
+})
+
+-- Load data
+local username = storage.get("username")
+local settings = storage.get("settings")
+
+-- Delete data
+storage.remove("username")
+
+-- Clear all data
+storage.clear()
+```
+
+## Navigation
+
+Navigate between screens in your app:
+
+```lua
+-- Navigate to screen
+navigateTo("settings") -- loads assets/settings.rml
+
+-- Go back
+goBack()
+
+-- Go to home screen
+goHome()
+
+-- Replace current screen (no back)
+replaceTo("login")
+```
+
+### Navigation Events
+
+```lua
+-- Listen for navigation
+onNavigate(function(screenName)
+ print("Navigated to: " .. screenName)
+end)
+
+-- Listen for back
+onBack(function()
+ print("Going back")
+end)
+```
+
+## HTTP Requests
+
+Make network requests (requires `network` permission):
+
+```lua
+-- GET request
+http.get("https://api.example.com/data", function(response)
+ if response.ok then
+ local data = json.decode(response.body)
+ print(data.message)
+ else
+ print("Error: " .. response.status)
+ end
+end)
+
+-- POST request
+http.post("https://api.example.com/submit", {
+ headers = {
+ ["Content-Type"] = "application/json"
+ },
+ body = json.encode({
+ name = "Alice",
+ action = "subscribe"
+ })
+}, function(response)
+ print("Status: " .. response.status)
+end)
+```
+
+## JSON
+
+```lua
+-- Parse JSON string
+local data = json.decode('{"name": "Alice", "age": 25}')
+print(data.name)
+
+-- Convert to JSON string
+local str = json.encode({
+ items = {"a", "b", "c"},
+ count = 3
+})
+```
+
+## Date and Time
+
+```lua
+-- Current timestamp
+local now = os.time()
+
+-- Format date
+local formatted = os.date("%Y-%m-%d %H:%M:%S", now)
+
+-- Parse date components
+local t = os.date("*t", now)
+print(t.year, t.month, t.day, t.hour, t.min, t.sec)
+```
+
+## Utilities
+
+### String Functions
+
+```lua
+-- Concatenation
+local greeting = "Hello, " .. name .. "!"
+
+-- String functions
+string.upper("hello") -- "HELLO"
+string.lower("HELLO") -- "hello"
+string.sub("hello", 1, 3) -- "hel"
+string.find("hello", "ll") -- 3
+string.gsub("hello", "l", "L") -- "heLLo"
+string.format("Score: %d", 100) -- "Score: 100"
+```
+
+### Math Functions
+
+```lua
+math.floor(3.7) -- 3
+math.ceil(3.2) -- 4
+math.round(3.5) -- 4
+math.abs(-5) -- 5
+math.min(1, 2, 3) -- 1
+math.max(1, 2, 3) -- 3
+math.random() -- 0-1
+math.random(1, 6) -- 1-6
+```
+
+### Table Functions
+
+```lua
+-- Insert
+table.insert(items, "new item")
+table.insert(items, 1, "at beginning")
+
+-- Remove
+table.remove(items) -- remove last
+table.remove(items, 1) -- remove first
+
+-- Sort
+table.sort(items)
+table.sort(items, function(a, b) return a > b end) -- descending
+
+-- Length
+local count = #items
+```
+
+## Sandbox Restrictions
+
+For security, these are **NOT** available:
+
+- `os.execute`, `io.popen` - No shell commands
+- `loadfile`, `dofile` - No arbitrary file loading
+- `require` - No external modules (use `import` for app modules)
+- `debug` library - No debugging hooks
+- `rawget`, `rawset` - No metatable bypass
+
+## Best Practices
+
+1. **Use local variables** - Faster and prevents pollution
+2. **Handle errors** - Use `pcall` for operations that might fail
+3. **Clean up timers** - Clear intervals when navigating away
+4. **Minimize DOM queries** - Cache element references
+5. **Batch updates** - Group style changes together
+
+### Error Handling
+
+```lua
+local success, result = pcall(function()
+ -- Code that might fail
+ local data = json.decode(invalidJson)
+ return data
+end)
+
+if success then
+ print("Parsed:", result)
+else
+ print("Error:", result)
+end
+```
+
+### Module Pattern
+
+```lua
+-- utils.lua
+local Utils = {}
+
+function Utils.formatCurrency(amount)
+ return string.format("$%.2f", amount)
+end
+
+function Utils.capitalize(str)
+ return str:sub(1,1):upper() .. str:sub(2)
+end
+
+return Utils
+```
+
+```lua
+-- main.lua
+local Utils = import("utils")
+
+print(Utils.formatCurrency(19.99))
+```
+
+## Next Steps
+
+- [Permissions Guide](permissions.md) - Request device capabilities
+- [API Reference](../api/lua-api.md) - Complete API documentation
+- [Debugging Guide](debugging.md) - Debug your Lua code
diff --git a/portal/internal/web/docs/guides/permissions.md b/portal/internal/web/docs/guides/permissions.md
new file mode 100644
index 0000000..8984a71
--- /dev/null
+++ b/portal/internal/web/docs/guides/permissions.md
@@ -0,0 +1,396 @@
+# Permissions Guide
+
+Mosis apps run in a secure sandbox with limited access to device features. To access sensitive capabilities, apps must declare permissions in their manifest.
+
+## Why Permissions?
+
+Permissions protect user privacy and security by:
+
+1. **Informing users** what an app can access before installation
+2. **Limiting damage** if an app misbehaves
+3. **Maintaining trust** in the Mosis ecosystem
+
+## Declaring Permissions
+
+Add permissions to your `manifest.json`:
+
+```json
+{
+ "id": "com.example.myapp",
+ "name": "My App",
+ "permissions": [
+ "storage",
+ "network"
+ ]
+}
+```
+
+Only request permissions your app actually needs. Users are more likely to trust apps with fewer permissions.
+
+## Available Permissions
+
+### storage
+
+**Description:** Persist data locally between app sessions.
+
+**Use cases:**
+- Save user preferences
+- Cache data for offline use
+- Store app state
+
+**API access:**
+```lua
+storage.set("key", value)
+storage.get("key")
+storage.remove("key")
+storage.clear()
+```
+
+**Note:** All apps have access to in-memory storage during a session. The `storage` permission enables persistence across sessions.
+
+---
+
+### network
+
+**Description:** Make HTTP/HTTPS requests to external servers.
+
+**Use cases:**
+- Fetch data from APIs
+- Submit form data
+- Load remote content
+
+**API access:**
+```lua
+http.get(url, callback)
+http.post(url, options, callback)
+http.request(options, callback)
+```
+
+**Restrictions:**
+- HTTPS only (HTTP blocked for security)
+- Cannot access localhost or internal IPs
+- Subject to CORS policies
+
+---
+
+### clipboard
+
+**Description:** Read from and write to the system clipboard.
+
+**Use cases:**
+- Copy text or data
+- Paste user content
+- Share functionality
+
+**API access:**
+```lua
+clipboard.write(text)
+clipboard.read(callback)
+```
+
+---
+
+### notifications
+
+**Description:** Display system notifications to the user.
+
+**Use cases:**
+- Reminders
+- Alerts
+- Background updates
+
+**API access:**
+```lua
+notifications.show({
+ title = "Reminder",
+ body = "Your timer is done!",
+ icon = "icons/alarm.png"
+})
+```
+
+**Restrictions:**
+- Notifications may be rate-limited
+- Users can disable notifications per-app
+
+---
+
+### camera
+
+**Description:** Capture photos using the device camera.
+
+**Use cases:**
+- Photo capture
+- QR code scanning
+- Augmented reality
+
+**API access:**
+```lua
+camera.capture({
+ quality = "high",
+ facing = "back"
+}, function(result)
+ if result.success then
+ local imageData = result.data
+ end
+end)
+```
+
+**Restrictions:**
+- User prompt before first access
+- Cannot record video (photo only)
+
+---
+
+### microphone
+
+**Description:** Record audio from the device microphone.
+
+**Use cases:**
+- Voice notes
+- Audio messages
+- Voice commands
+
+**API access:**
+```lua
+microphone.start()
+microphone.stop(function(result)
+ local audioData = result.data
+end)
+```
+
+**Restrictions:**
+- User prompt before first access
+- Maximum recording duration enforced
+
+---
+
+### location
+
+**Description:** Access device location information.
+
+**Use cases:**
+- Weather apps
+- Maps
+- Location-based features
+
+**API access:**
+```lua
+location.get(function(result)
+ if result.success then
+ print(result.latitude, result.longitude)
+ end
+end)
+
+location.watch(function(result)
+ -- Called on location changes
+end)
+```
+
+**Restrictions:**
+- User prompt before first access
+- Approximate location only (no precise GPS)
+- Battery impact warning
+
+---
+
+### contacts
+
+**Description:** Read device contacts.
+
+**Use cases:**
+- Contact picker
+- Address book integration
+- Sharing with friends
+
+**API access:**
+```lua
+contacts.pick(function(result)
+ if result.success then
+ print(result.name, result.phone)
+ end
+end)
+
+contacts.getAll(function(result)
+ for i, contact in ipairs(result.contacts) do
+ print(contact.name)
+ end
+end)
+```
+
+**Restrictions:**
+- Read-only access
+- User prompt before first access
+
+## Permission Levels
+
+| Level | Description | Example |
+|-------|-------------|---------|
+| **Normal** | Low risk, minimal review | storage |
+| **Sensitive** | Requires user prompt | camera, microphone, location |
+| **Dangerous** | Extensive review required | contacts |
+
+## Runtime Behavior
+
+### First-Time Prompts
+
+Some permissions trigger a user prompt on first use:
+
+```lua
+-- First call triggers prompt
+camera.capture(options, function(result)
+ if result.denied then
+ -- User denied permission
+ showPermissionExplanation()
+ elseif result.success then
+ -- Permission granted
+ handlePhoto(result.data)
+ end
+end)
+```
+
+### Checking Permission Status
+
+```lua
+-- Check if permission is granted
+if permissions.check("camera") then
+ -- Already have permission
+ showCameraButton()
+else
+ -- Need to request
+ showRequestButton()
+end
+```
+
+### Requesting at Runtime
+
+```lua
+permissions.request("camera", function(granted)
+ if granted then
+ startCamera()
+ else
+ showAlternative()
+ end
+end)
+```
+
+## Best Practices
+
+### 1. Minimize Permissions
+
+Only request what you need. An app with fewer permissions:
+- Builds more user trust
+- Passes review faster
+- Has smaller attack surface
+
+### 2. Request at the Right Time
+
+Don't request all permissions at startup. Request when the user takes an action that needs it:
+
+```lua
+-- Bad: Request on app start
+function onAppStart()
+ permissions.request("camera") -- Why?
+end
+
+-- Good: Request when needed
+function onTakePhotoClicked()
+ permissions.request("camera", function(granted)
+ if granted then
+ camera.capture(options, handlePhoto)
+ end
+ end)
+end
+```
+
+### 3. Explain Why
+
+Tell users why you need a permission before requesting:
+
+```xml
+
+
This app needs camera access to scan QR codes.
+
+
+```
+
+### 4. Handle Denial Gracefully
+
+Apps should work (with reduced functionality) even if permissions are denied:
+
+```lua
+function capturePhoto()
+ if not permissions.check("camera") then
+ -- Offer alternative
+ showManualEntryOption()
+ return
+ end
+ -- Proceed with camera
+end
+```
+
+### 5. Don't Ask Again Immediately
+
+If a user denies a permission, don't immediately ask again:
+
+```lua
+local lastDenied = storage.get("camera_denied_time")
+if lastDenied and os.time() - lastDenied < 86400 then
+ -- Wait at least 24 hours before asking again
+ return
+end
+```
+
+## Review Impact
+
+Permission requests affect app review:
+
+| Permission | Review Impact |
+|------------|---------------|
+| storage, network | Automatic approval |
+| clipboard | Quick review |
+| notifications | Standard review |
+| camera, microphone | Extended review |
+| location | Extended review |
+| contacts | Manual review required |
+
+Apps requesting sensitive permissions must:
+1. Justify the need in submission notes
+2. Use the permission appropriately
+3. Respect user privacy
+
+## Troubleshooting
+
+### "Permission not declared"
+
+```
+Error: Cannot use camera without 'camera' permission
+```
+
+Add the permission to your manifest:
+```json
+"permissions": ["camera"]
+```
+
+### "Permission denied by user"
+
+Handle this gracefully in your code:
+```lua
+if result.denied then
+ showAlternativeUI()
+end
+```
+
+### "Permission blocked"
+
+The user permanently blocked the permission. Direct them to settings:
+```lua
+if result.blocked then
+ showMessage("Please enable camera in system settings")
+end
+```
+
+## See Also
+
+- [Manifest Reference](../api/manifest.md) - Full manifest documentation
+- [Security Guide](security.md) - App security best practices
+- [Publishing Guide](publishing.md) - App review process
diff --git a/portal/internal/web/docs/guides/ui-design.md b/portal/internal/web/docs/guides/ui-design.md
new file mode 100644
index 0000000..27787c0
--- /dev/null
+++ b/portal/internal/web/docs/guides/ui-design.md
@@ -0,0 +1,395 @@
+# UI Design Guide
+
+Mosis uses RML (RmlUi Markup Language) and RCSS (RmlUi CSS) for building user interfaces. If you know HTML and CSS, you'll feel right at home.
+
+## RML Basics
+
+RML is similar to HTML but with some differences optimized for UI rendering.
+
+### Document Structure
+
+```xml
+
+
+ App Title
+
+
+
+
+
+
+```
+
+### Common Elements
+
+| Element | Usage |
+|---------|-------|
+| `