Quickstarts

Full Stack App

Build a Userland app with static UI, server routes, auth, data, and secrets.

For agents: Model durable needs in resources, keep secret reads in server code, and use ctx helpers instead of direct platform APIs.

File Structure

manifest.userland.json
public/index.html
public/assets/app.js
public/assets/app.css
server/index.js

Resource Shape

Declare auth, data, and secrets before writing runtime code that depends on them. If activation returns pending_secrets, set the missing secrets and republish or inspect the release state.

Manifest

{
  "app": {
    "name": "Team Notes",
    "summary": "A private notes app with admin-managed access.",
    "visibility": "public"
  },
  "runtime": {
    "static_root": "public",
    "server_entry": "server/index.js",
    "fallback": "server"
  },
  "resources": {
    "auth": {
      "mode": "app_users",
      "roles": ["admin", "editor"],
      "public_signup": false
    },
    "data": {
      "collections": {
        "notes": {
          "fields": {
            "title": "string",
            "body": "richtext",
            "status": { "type": "enum", "values": ["draft", "published"] }
          },
          "indexes": [{ "name": "by_status", "fields": ["status"] }],
          "access": { "read": "authenticated", "write": "role:editor" }
        }
      }
    },
    "secrets": { "required": ["OPENAI_API_KEY"] }
  }
}

Server entry

export default {
  async fetch(request, ctx) {
    const url = new URL(request.url);
    const notes = ctx.data.collection("notes");

    if (url.pathname === "/api/notes" && request.method === "GET") {
      await ctx.auth.requireRole(request, "editor");
      return Response.json(await notes.list({ limit: 50 }));
    }

    if (url.pathname === "/api/notes" && request.method === "POST") {
      await ctx.auth.requireRole(request, "editor");
      const input = await request.json();
      const note = await notes.create({
        title: input.title,
        body: input.body,
        status: "draft"
      });
      return Response.json({ note }, { status: 201 });
    }

    return new Response("not found", { status: 404 });
  }
};

Publish and operate

npm run userland -- apps publish . --message "Initial Team Notes release"
printf '%s' "$OPENAI_API_KEY" | npm run userland -- apps secrets set "$APP_ID" OPENAI_API_KEY
npm run userland -- apps events "$APP_ID" --severity error --limit 25

Verification