# Your first app

> Build, run and register a Facia app in about 15 minutes, with a Node server, one page, token verification and a coin call.

# Your first app

We'll build **Daily Bonus**: a tab where a Verified member presses a button once a day and gets 10 coins. It uses everything an app needs: the token, the theme, and one partner call.

You need **Node 20+** and a business where you're an owner or admin, with **Apps** and **Client coins** on in [Features](https://docs.facia.chat/business/features).

## 1. The server

```bash
mkdir daily-bonus && cd daily-bonus
npm init -y
npm install express jsonwebtoken dotenv
```

`server.js`:

```js
import "dotenv/config";
import express from "express";
import jwt from "jsonwebtoken";

const { FACIA_APP_ID, FACIA_APP_SECRET, FACIA_API, PORT = 3000 } = process.env;
const app = express();
app.use(express.json());
app.use(express.static("public"));

// Verify the token Facia gave the page. Never trust it without this.
function client(req) {
  return jwt.verify(req.body.token, FACIA_APP_SECRET, {
    algorithms: ["HS256"],
    issuer: "facia",
    audience: FACIA_APP_ID,
  });
}

// Call Facia's partner API from the server, with the secret.
async function facia(call, body) {
  const res = await fetch(`${FACIA_API}/${call}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${FACIA_APP_SECRET}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  return res.json();
}

app.post("/api/me", (req, res) => {
  try {
    const c = client(req);
    res.json({ alias: c.alias, tier: c.tier, coins: c.coins });
  } catch {
    res.status(401).json({ error: "bad_token" });
  }
});

app.post("/api/claim", async (req, res) => {
  let c;
  try { c = client(req); } catch { return res.status(401).json({ error: "bad_token" }); }
  if (c.tier !== "verified" && c.tier !== "vip") return res.json({ ok: false, error: "verified_only" });
  const day = new Date().toISOString().slice(0, 10);
  // The ref makes it once per client per day: a second claim gets the first answer back.
  const out = await facia("coins.pay", { sub: c.sub, amount: 10, reason: "Daily bonus", ref: `bonus:${c.biz}:${c.sub}:${day}` });
  res.json(out);
});

app.listen(PORT, () => console.log(`Daily Bonus on http://localhost:${PORT}`));
```

Add `"type": "module"` to `package.json` so the `import` lines work.

## 2. The page

`public/index.html`:

```html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Bonus</title>
<style>
  html, body { margin: 0; background: var(--bg, #fff); color: var(--text, #111); font-family: system-ui, sans-serif; }
  main { padding: 32px 20px; text-align: center; }
  button { font: inherit; font-weight: 600; padding: 14px 28px; border: 0; border-radius: 14px;
           background: var(--accent, #3d6bff); color: var(--accent-fg, #fff); }
  [data-facia-radius="sharp"] button { border-radius: 4px; }
  p { color: var(--text-2, #555); }
</style>
</head>
<body>
<main>
  <h1 id="hi">Daily Bonus</h1>
  <p id="msg">Loading…</p>
  <button id="claim" hidden>Claim 10 coins</button>
</main>
<script src="https://facia.chat/app/facia-app.js"></script>
<script>
  Facia.onTheme((t) => Facia.applyTheme(t));
  const post = (url) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json" },
                                     body: JSON.stringify({ token: Facia.token() }) }).then((r) => r.json());

  post("/api/me").then((me) => {
    document.getElementById("hi").textContent = `Hi ${me.alias}`;
    document.getElementById("msg").textContent = `You have ${me.coins} coins.`;
    document.getElementById("claim").hidden = false;
    Facia.ready(); // drop Facia's loading skeleton once something is drawn
  });

  document.getElementById("claim").onclick = async () => {
    const r = await post("/api/claim");
    document.getElementById("msg").textContent =
      r.ok ? (r.repeat ? "Already claimed today. Come back tomorrow!" : `+10! You now have ${r.balance}.`)
           : r.error === "verified_only" ? "Get verified to claim daily coins." : "Something went wrong.";
  };
</script>
</body>
</html>
```

## 3. Run it where Facia can reach it

While building, Facia accepts `http://localhost` addresses (also `127.0.0.1` and `*.localhost`), so you can use your own computer when *you* test from the same machine:

```bash
node server.js
```

To let real clients use it, deploy it anywhere with **https**: Render, Railway, Fly.io, Cloudflare, a VPS… Any host that runs Node works.

## 4. Register it in HQ

![HQ → Settings → Apps](https://docs.facia.chat/_assets/shots/settings-apps.webp)

1. **HQ → Settings → Apps → Add** (under **Your apps**).
2. Fill in:
   - **Name**: Daily Bonus. **Icon**: a square image (optional).
   - **Page address**: `https://your-app.example.com` (or `http://localhost:3000` while building).
   - **Who sees it**: Verified members.
   - **Where it shows**: Membership card, and/or a tab in the chat header, and/or the Verified group's Apps page.
   - **What it may do**: tick **Give coins**.
3. Press **Add app**. Facia shows the **app secret once**. Press **Copy as .env** and paste it into a `.env` file next to `server.js`:

```bash
FACIA_APP_ID=…
FACIA_APP_SECRET=fas_…
FACIA_API=https://site.facia.chat/partner/v1
```

Use the exact values HQ gives you. Restart the server.

> [!WARNING]
> The secret is shown once. Add `.env` to `.gitignore`. If it leaks, press **New secret** on the app in HQ, and the old one stops working at once.

## 5. Try it

Open your business as a Verified client (or as staff: the team reads as `verified`), go where you placed it, and press the button. Check **HQ → Activity**: you'll see "app.pay" with the app's name, the amount and the ref.

## What you can switch in HQ later

Each app's row in HQ shows the secret's last 4 characters and how many times it was opened this week. It also has:

- a **kill switch** (off = the tab disappears and every call is refused at once),
- **New secret** (the old one stops working at once),
- **Edit** (address, audience, places, permissions, groups),
- **Delete**.

## Next

- Every claim in the token: [The token](https://docs.facia.chat/build/token)
- Theme, insets and messages: [The frame bridge](https://docs.facia.chat/build/bridge)
- All the calls: [Partner API](https://docs.facia.chat/build/partner-api)
- More examples: [Recipes](https://docs.facia.chat/build/recipes)
