# Quickstart: your first app

You will build a small notes app: an Express server that stores notes in an edge SQLite database, lets the browser upload attachments straight to object storage, and sells an upgrade through a hosted checkout. It deploys as a server app at `https://<slug>.harakumo.app`.

Server apps are on Pro and up: on the free Hobby plan the deploy step (step 5) answers 402 before anything builds, while the database, storage and payment steps work as written. To go live on Hobby, put the same logic in a static site that calls functions, which are free: the Deployments page says how a static site is recognized, and the Functions page how to write and deploy one.

Replace the values in angle brackets with the ones each step prints.

## 1. Sign up and create a setup key

1. Sign up at harakumo.com/signup and enter the 6-digit code emailed to you. Verify now: live payment links need a verified workspace-owner email.
2. Open Dashboard → Settings → API access and create a key with full access to the whole workspace. It stays on your machine for setup.

**Terminal**

```bash
npm install -g @harakumo/cli
export HARAKUMO_API_KEY=hk_live_…          # the key you just created
harakumo login --key $HARAKUMO_API_KEY
```

## 2. Create the project, a database, a bucket and a payment account

**Terminal**

```bash
harakumo projects create notes-app       # ✓ Created project notes-app #<projectId>
harakumo db create <projectId> notes     # ✓ Creating notes (sqlite, #<dbId>)
harakumo db query <dbId> "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL, attachment TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"
harakumo storage create <projectId> uploads
harakumo storage <projectId>             # lists buckets with their ids: note <bucketId>

# Payments are live as soon as they are enabled:
curl -X POST https://harakumo.com/api/projects/<projectId>/payments \
  -H "Authorization: Bearer $HARAKUMO_API_KEY"
# → { "account": { "id": <accountId>, … } }
```

## 3. Give the app its own key

Your setup key can do anything in the workspace; the deployed app should not. In Settings → API access create a second key with full access and Project set to notes-app. It works on this project's database, buckets and payment account and is refused everywhere else.

Store it and the resource ids as the project's environment variables — in the dashboard (project → Environment) or with the API:

| Variable | Value |
| --- | --- |
| HARAKUMO_API_KEY | the project key (hk_live_…) |
| NOTES_DB_ID | `<dbId>` |
| UPLOADS_BUCKET_ID | `<bucketId>` |
| PAYMENTS_ACCOUNT_ID | `<accountId>` |
| APP_URL | `https://<slug>.harakumo.app` (the slug is on the project page) |

**One variable with curl**

```bash
curl -X POST https://harakumo.com/api/projects/<projectId>/env \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{"key":"NOTES_DB_ID","value":"<dbId>"}'
```

> Environment variables are copied into the app when it deploys. After changing one, redeploy.

## 4. Write the app

Three files in a folder called notes-app. Keep the page in `web/`: an index.html in the folder root, or in public/, dist/, build/, out/, _site/, site/ or docs/, makes the pipeline serve the folder as a static site instead of starting your server.

**package.json**

```json
{
  "name": "notes-app",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": { "@harakumo/sdk": "latest", "express": "^4.21.2" }
}
```

**server.js**

```js
import express from 'express';
import Harakumo from '@harakumo/sdk';

const hk = new Harakumo({ apiKey: process.env.HARAKUMO_API_KEY });
const DB = Number(process.env.NOTES_DB_ID);
const BUCKET = Number(process.env.UPLOADS_BUCKET_ID);
const PAYMENTS = Number(process.env.PAYMENTS_ACCOUNT_ID);
const APP_URL = process.env.APP_URL;

const app = express();
app.use(express.json());
app.use(express.static('web'));

// Notes live in the edge SQLite database; every query is one HTTPS call.
app.get('/api/notes', async (req, res) => {
  const { results } = await hk.databases.query(DB,
    'SELECT id, body, attachment, created_at FROM notes ORDER BY id DESC LIMIT 50');
  res.json(results);
});

app.post('/api/notes', async (req, res) => {
  const { body, attachment = null } = req.body;
  await hk.databases.query(DB, 'INSERT INTO notes (body, attachment) VALUES (?, ?)', [String(body), attachment]);
  res.status(201).json({ ok: true });
});

// The browser uploads straight to storage with a signed URL; the bytes never touch this server.
app.post('/api/upload-url', async (req, res) => {
  const { filename, contentType } = req.body;
  const key = 'attachments/' + Date.now() + '-' + String(filename).replace(/[^a-zA-Z0-9._-]/g, '_');
  const { url } = await hk.storage.presign(BUCKET, { key, method: 'put', contentType });
  res.json({ url, key });
});

// A hosted checkout. The buyer comes back to /thanks?session_id=…
app.post('/api/checkout', async (req, res) => {
  const { url } = await hk.payments.createSession(PAYMENTS, {
    amountCents: 500,
    description: 'Notes Pro',
    reference: 'user-' + String(req.body.userId || 'anonymous'),
    successUrl: APP_URL + '/thanks',
    cancelUrl: APP_URL + '/',
  });
  res.json({ url });
});

// Confirm on the server; never trust the redirect alone.
app.get('/thanks', async (req, res) => {
  const r = await fetch('https://harakumo.com/api/payments/' + PAYMENTS + '/sessions/'
    + encodeURIComponent(req.query.session_id || ''), {
    headers: { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY },
  });
  const { paid, payment } = await r.json();
  res.send(paid ? 'Paid $' + (payment.amount_cents / 100).toFixed(2) + ' — thank you!' : 'Payment not completed yet.');
});

app.listen(process.env.PORT || 3000);
```

**web/index.html**

```html
<!doctype html>
<title>Notes</title>
<form id="note"><input name="body" placeholder="Write a note" required> <input type="file" name="file"> <button>Save</button></form>
<ul id="list"></ul>
<button id="buy">Upgrade for $5</button>
<script type="module">
const json = (url, body) => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());

async function load() {
  const notes = await (await fetch('/api/notes')).json();
  document.getElementById('list').replaceChildren(...notes.map(n => {
    const li = document.createElement('li');
    li.textContent = n.body + (n.attachment ? ' (attachment)' : '');
    return li;
  }));
}

document.getElementById('note').onsubmit = async (e) => {
  e.preventDefault();
  const form = new FormData(e.target);
  const file = form.get('file');
  let attachment = null;
  if (file && file.size) {
    const type = file.type || 'application/octet-stream';
    const { url, key } = await json('/api/upload-url', { filename: file.name, contentType: type });
    await fetch(url, { method: 'PUT', headers: { 'content-type': type }, body: file });  // same Content-Type as signed
    attachment = key;
  }
  await json('/api/notes', { body: form.get('body'), attachment });
  e.target.reset();
  load();
};

document.getElementById('buy').onclick = async () => {
  location.href = (await json('/api/checkout', {})).url;
};
load();
</script>
```

## 5. Deploy (needs Pro and up)

This step needs Pro and up: the notes app is a server app. On the free Hobby plan the deploy answers 402 `plan_limit` (with `required_plan`) and nothing is built. Everything you created in steps 1 to 4 stays and works; upgrade the workspace on Dashboard → Billing to deploy this app, or serve the page as a static site and move its routes into functions, which deploy on Hobby.

**Terminal**

```bash
harakumo deploy <projectId> ./notes-app    # uploads the folder
harakumo deploys <projectId>               # queued → building → deploying → ready
```

> Deploy the folder without node_modules: an upload is at most 25 MB, and the build machine installs dependencies itself. Keep secrets in environment variables, not in a .env file.

## What happens

The pipeline finds a package.json with a start script and no static index.html, installs the dependencies on a build machine and runs `npm start` in a container. The first deploy takes a few minutes. When the status is ready, open `https://<slug>.harakumo.app`.

- A server app listens on `process.env.PORT`; the platform sets it.
- An idle app sleeps after 15 minutes. The next visitor sees a short starting page while it wakes.
- The container's disk is wiped on sleep and on every deploy. Keep data in the database and storage, as this app does.

## 6. Take a payment

Press Upgrade on the live app. The hosted checkout charges a real card: to try the flow, pay yourself and refund it from Dashboard → Payments (refunds are for the workspace owner). You return to /thanks?session_id=…, and the server confirms the payment with the session lookup.

The payment appears in Dashboard → Payments as Earnings: $5.00 less the 3.9% + 30¢ fee. Each payment is held 7 days before the workspace owner can withdraw it.

## Next

- Databases — batch statements, export, limits and errors.
- Storage — download links, listings and folders.
- Payments — webhooks, metadata and refunds.
- Auth pools — add sign-in for your users.
- Limits & scaling — what this app can handle, and where the walls are.
