---
# System prepended metadata

title: PocketBase 入門到實作：從零到上線的完整教學
tags: [pocketbase, tutorial, backend, api, sqlite]

---

---
tags:
  - backend
  - pocketbase
  - tutorial
  - sqlite
  - api
aliases:
  - PocketBase 教學
  - PocketBase 實作
created: 2024-12-22
updated: 2024-12-22
---

# PocketBase 入門到實作：從零到上線的完整教學

![pocketbase-tutorial-cover](https://hackmd.io/_uploads/SJEPpqw7Zl.jpg)

## 前言

上一篇介紹了各種 Supabase 替代品，這篇要深入 PocketBase 的實際操作。從安裝、設定、前端整合到部署上線，完整走一遍流程。

PocketBase 的特點是簡單直接。不需要 Docker，不需要資料庫安裝，下載一個檔案就能跑。這讓它特別適合：

- 想快速驗證想法的個人開發者
- 學習後端開發的初學者
- 需要輕量後端的小型專案
- 不想維護複雜基礎設施的團隊

這篇教學會涵蓋：

1. 安裝與啟動
2. Admin 後台操作
3. 資料庫設計
4. 前端整合（含完整程式碼）
5. 認證系統
6. 自訂 API
7. 部署到線上
8. 實戰案例：待辦清單應用

---

## 環境準備

PocketBase 是單一執行檔，不需要預先安裝任何東西。支援的作業系統：

- Linux（amd64、arm64）
- macOS（Intel、Apple Silicon）
- Windows

---

## 第一步：安裝與啟動

### 方法一：直接下載

```bash
# macOS (Apple Silicon)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_arm64.zip

# macOS (Intel)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_amd64.zip

# Linux
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_linux_amd64.zip

# 解壓縮
unzip pocketbase_*.zip

# 啟動
./pocketbase serve
```

啟動後會看到：

```
2024/01/01 12:00:00 Server started at http://127.0.0.1:8090
  - REST API: http://127.0.0.1:8090/api/
  - Admin UI: http://127.0.0.1:8090/_/
```

### 方法二：Docker

如果偏好容器化部署：

```dockerfile
FROM alpine:latest

ARG PB_VERSION=0.28.4

RUN apk add --no-cache unzip ca-certificates

ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip
RUN unzip /tmp/pb.zip -d /pb/

EXPOSE 8080

CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"]
```

```bash
docker build -t pocketbase .
docker run -d -p 8080:8080 -v $(pwd)/pb_data:/pb/pb_data pocketbase
```

### 首次設定

開啟 `http://127.0.0.1:8090/_/` 會看到管理員帳號設定頁面。填入 email 和密碼後，就能進入 Admin 後台。

這個帳號是超級管理員（Superuser），擁有所有權限。實際專案中請使用強密碼。

---

## 第二步：認識 Admin 後台

Admin 後台分為幾個主要區塊：

### Collections（資料集合）

類似資料庫的 Table。每個 Collection 包含：

- **欄位定義**：支援多種類型（Text、Number、Bool、Date、File、Relation 等）
- **API 規則**：控制 CRUD 權限
- **索引設定**：優化查詢效能

PocketBase 預設有兩個系統 Collection：

- `_superusers`：管理員帳號
- `users`：一般使用者（Auth Collection）

### Logs

查看 API 請求紀錄、錯誤訊息。

### Settings

全域設定，包含：

- 應用程式名稱
- SMTP 設定（用於發送驗證信）
- 檔案儲存設定（本地或 S3）
- 認證選項
- 備份設定

---

## 第三步：建立第一個 Collection

以部落格文章為例，建立 `posts` Collection：

### 在 Admin 後台操作

1. 點擊「New collection」
2. 輸入名稱：`posts`
3. 類型選擇：「Base」（一般資料集合）
4. 新增欄位：

| 欄位名稱 | 類型 | 設定 |
|---------|------|------|
| title | Text | Required |
| content | Editor | Required |
| slug | Text | Required, Unique |
| status | Select | Options: draft, published |
| author | Relation | 關聯到 users |
| cover | File | 允許圖片類型 |
| published_at | DateTime | - |

### 設定 API 規則

在 Collection 設定的「API Rules」區塊：

```javascript
// List/Search Rule - 只有已發布的文章對外公開
status = "published"

// View Rule - 同上，或作者本人可看草稿
status = "published" || author = @request.auth.id

// Create Rule - 只有登入使用者可建立
@request.auth.id != ""

// Update Rule - 只有作者可編輯
author = @request.auth.id

// Delete Rule - 只有作者可刪除
author = @request.auth.id
```

這些規則用類似 SQL WHERE 的語法，`@request.auth` 代表當前登入的使用者。

---

## 第四步：前端整合

### 安裝 SDK

```bash
npm install pocketbase
```

### 初始化

```javascript
import PocketBase from 'pocketbase';

const pb = new PocketBase('http://127.0.0.1:8090');

// 如果在 Node.js 環境，需要處理 localStorage
// pb.authStore = new BaseAuthStore();
```

### CRUD 操作

#### 建立記錄

```javascript
const record = await pb.collection('posts').create({
    title: '我的第一篇文章',
    content: '<p>這是內容...</p>',
    slug: 'my-first-post',
    status: 'draft',
    author: pb.authStore.record.id
});

console.log(record.id); // 自動生成的 ID
```

#### 查詢記錄

```javascript
// 取得列表（分頁）
const resultList = await pb.collection('posts').getList(1, 20, {
    filter: 'status = "published"',
    sort: '-created',
    expand: 'author'
});

console.log(resultList.items);
console.log(resultList.totalItems);
console.log(resultList.totalPages);

// 取得單筆
const record = await pb.collection('posts').getOne('RECORD_ID', {
    expand: 'author'
});

// 用其他欄位查詢
const post = await pb.collection('posts').getFirstListItem(
    `slug = "my-first-post"`
);
```

#### 更新記錄

```javascript
const updated = await pb.collection('posts').update('RECORD_ID', {
    title: '更新後的標題',
    status: 'published',
    published_at: new Date().toISOString()
});
```

#### 刪除記錄

```javascript
await pb.collection('posts').delete('RECORD_ID');
```

### 檔案上傳

```javascript
// 建立 FormData
const formData = new FormData();
formData.append('title', '帶圖片的文章');
formData.append('content', '<p>內容</p>');
formData.append('slug', 'post-with-image');
formData.append('status', 'draft');
formData.append('author', pb.authStore.record.id);
formData.append('cover', fileInput.files[0]); // File 物件

const record = await pb.collection('posts').create(formData);

// 取得檔案 URL
const coverUrl = pb.files.getUrl(record, record.cover);
// 加上尺寸參數（自動縮放）
const thumbUrl = pb.files.getUrl(record, record.cover, { thumb: '300x200' });
```

### 即時訂閱

PocketBase 支援 SSE（Server-Sent Events）實現即時資料同步：

```javascript
// 訂閱整個 Collection
pb.collection('posts').subscribe('*', function (e) {
    console.log(e.action); // 'create' | 'update' | 'delete'
    console.log(e.record);
});

// 訂閱特定記錄
pb.collection('posts').subscribe('RECORD_ID', function (e) {
    console.log('記錄已更新:', e.record);
});

// 取消訂閱
pb.collection('posts').unsubscribe('*');
pb.collection('posts').unsubscribe('RECORD_ID');
pb.collection('posts').unsubscribe(); // 取消此 Collection 所有訂閱
```

---

## 第五步：認證系統

### 使用者註冊

```javascript
const user = await pb.collection('users').create({
    email: 'user@example.com',
    password: '12345678',
    passwordConfirm: '12345678',
    name: '使用者名稱'
});

// 發送驗證信（需要先設定 SMTP）
await pb.collection('users').requestVerification('user@example.com');
```

### 登入

```javascript
// Email + 密碼
const authData = await pb.collection('users').authWithPassword(
    'user@example.com',
    '12345678'
);

console.log(pb.authStore.isValid);  // true
console.log(pb.authStore.token);    // JWT
console.log(pb.authStore.record);   // 使用者資料
```

### OAuth2 登入

需要先在 Admin 後台設定 OAuth2 提供者（Settings → Auth providers）。

```javascript
// 自動處理彈窗流程
const authData = await pb.collection('users').authWithOAuth2({
    provider: 'google'
});
```

### 登出

```javascript
pb.authStore.clear();
```

### 檢查登入狀態

```javascript
if (pb.authStore.isValid) {
    console.log('已登入:', pb.authStore.record.email);
} else {
    console.log('未登入');
}

// 監聽狀態變化
pb.authStore.onChange((token, record) => {
    console.log('認證狀態改變');
});
```

### 刷新 Token

```javascript
// Token 即將過期時刷新
if (pb.authStore.isValid) {
    await pb.collection('users').authRefresh();
}
```

---

## 第六步：自訂 API 路由

有時候內建的 CRUD API 不夠用，需要自訂邏輯。

### 使用 JavaScript Hooks

在 `pb_hooks` 目錄下建立 `.js` 檔案：

```javascript
// pb_hooks/custom_routes.pb.js

// 自訂 GET 路由
routerAdd("GET", "/api/custom/stats", (e) => {
    // 執行 SQL 查詢
    const result = $app.db()
        .newQuery(`
            SELECT
                COUNT(*) as total,
                SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) as published
            FROM posts
        `)
        .one();

    return e.json(200, result);
}, $apis.requireAuth()); // 需要認證

// 自訂 POST 路由
routerAdd("POST", "/api/custom/publish/{id}", (e) => {
    const id = e.request.pathValue("id");
    const record = $app.findRecordById("posts", id);

    // 檢查權限
    if (record.get("author") !== e.auth.id) {
        throw new ForbiddenError("只有作者可以發布");
    }

    record.set("status", "published");
    record.set("published_at", new Date().toISOString());
    $app.save(record);

    return e.json(200, record);
}, $apis.requireAuth());
```

### Hooks（事件鉤子）

```javascript
// pb_hooks/hooks.pb.js

// 建立記錄前
onRecordCreate((e) => {
    // 自動設定 slug
    if (!e.record.get("slug")) {
        const title = e.record.get("title");
        const slug = title.toLowerCase().replace(/\s+/g, '-');
        e.record.set("slug", slug);
    }
    return e.next();
}, "posts");

// 建立記錄後
onRecordAfterCreateSuccess((e) => {
    console.log("新文章建立:", e.record.id);
    // 可以在這裡發送通知、更新快取等
}, "posts");

// 刪除記錄前
onRecordDelete((e) => {
    // 阻止刪除已發布的文章
    if (e.record.get("status") === "published") {
        throw new BadRequestError("無法刪除已發布的文章");
    }
    return e.next();
}, "posts");
```

---

## 第七步：部署到線上

### 方法一：VPS 直接部署

適合：個人專案、小型應用

```bash
# 1. 上傳執行檔和 pb_data 到伺服器
scp pocketbase user@server:/opt/pocketbase/
scp -r pb_data user@server:/opt/pocketbase/

# 2. SSH 到伺服器
ssh user@server

# 3. 設定 systemd 服務
sudo nano /etc/systemd/system/pocketbase.service
```

```systemd
[Unit]
Description=PocketBase
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve yourdomain.com
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
# 4. 啟動服務
sudo systemctl daemon-reload
sudo systemctl enable pocketbase
sudo systemctl start pocketbase

# 5. 檢查狀態
sudo systemctl status pocketbase
```

使用 `yourdomain.com` 作為參數時，PocketBase 會自動處理 Let's Encrypt 憑證。

### 方法二：Docker + Nginx

適合：需要更多控制、多服務架構

```yaml
# docker-compose.yml
version: '3.8'

services:
  pocketbase:
    build: .
    container_name: pocketbase
    volumes:
      - ./pb_data:/pb/pb_data
      - ./pb_hooks:/pb/pb_hooks
    restart: unless-stopped
    networks:
      - web

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certs:/etc/nginx/certs
    depends_on:
      - pocketbase
    networks:
      - web

networks:
  web:
```

### 方法三：Railway / Fly.io / Render

這些 PaaS 平台支援 Docker 部署，適合不想管理伺服器的情況。

以 Fly.io 為例：

```toml
# fly.toml
app = "my-pocketbase"
primary_region = "nrt"  # 東京

[build]
  dockerfile = "Dockerfile"

[mounts]
  source = "pb_data"
  destination = "/pb/pb_data"

[http_service]
  internal_port = 8080
  force_https = true
```

```bash
fly launch
fly deploy
```

---

## 實戰案例：待辦清單應用

把前面學的整合起來，建立一個完整的待辦清單應用。

### 資料結構

建立 `todos` Collection：

| 欄位 | 類型 | 設定 |
|------|------|------|
| title | Text | Required |
| completed | Bool | Default: false |
| user | Relation | 關聯 users, Required |
| due_date | DateTime | - |

API 規則：

```javascript
// 所有操作都限制為擁有者
List:   user = @request.auth.id
View:   user = @request.auth.id
Create: @request.auth.id != "" && @request.body.user = @request.auth.id
Update: user = @request.auth.id
Delete: user = @request.auth.id
```

### 前端程式碼（Vue 3 範例）

```vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import PocketBase from 'pocketbase'

const pb = new PocketBase('http://127.0.0.1:8090')

const todos = ref([])
const newTodo = ref('')
const loading = ref(false)

// 載入待辦事項
async function loadTodos() {
  loading.value = true
  try {
    const records = await pb.collection('todos').getFullList({
      sort: '-created',
      filter: `user = "${pb.authStore.record.id}"`
    })
    todos.value = records
  } finally {
    loading.value = false
  }
}

// 新增待辦
async function addTodo() {
  if (!newTodo.value.trim()) return

  const record = await pb.collection('todos').create({
    title: newTodo.value,
    completed: false,
    user: pb.authStore.record.id
  })

  todos.value.unshift(record)
  newTodo.value = ''
}

// 切換完成狀態
async function toggleTodo(todo) {
  const updated = await pb.collection('todos').update(todo.id, {
    completed: !todo.completed
  })

  const index = todos.value.findIndex(t => t.id === todo.id)
  todos.value[index] = updated
}

// 刪除待辦
async function deleteTodo(todo) {
  await pb.collection('todos').delete(todo.id)
  todos.value = todos.value.filter(t => t.id !== todo.id)
}

// 即時同步
let unsubscribe

onMounted(async () => {
  await loadTodos()

  // 訂閱變更
  unsubscribe = await pb.collection('todos').subscribe('*', (e) => {
    if (e.action === 'create') {
      // 避免重複新增自己建立的
      if (!todos.value.find(t => t.id === e.record.id)) {
        todos.value.unshift(e.record)
      }
    } else if (e.action === 'update') {
      const index = todos.value.findIndex(t => t.id === e.record.id)
      if (index !== -1) {
        todos.value[index] = e.record
      }
    } else if (e.action === 'delete') {
      todos.value = todos.value.filter(t => t.id !== e.record.id)
    }
  })
})

onUnmounted(() => {
  unsubscribe?.()
})
</script>

<template>
  <div class="todo-app">
    <h1>待辦清單</h1>

    <form @submit.prevent="addTodo" class="add-form">
      <input
        v-model="newTodo"
        placeholder="新增待辦事項..."
        :disabled="loading"
      />
      <button type="submit" :disabled="loading || !newTodo.trim()">
        新增
      </button>
    </form>

    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="toggleTodo(todo)"
        />
        <span>{{ todo.title }}</span>
        <button @click="deleteTodo(todo)" class="delete">×</button>
      </li>
    </ul>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.add-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.add-form input {
  flex: 1;
  padding: 10px;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.delete {
  margin-left: auto;
  background: #ff4444;
  color: white;
  border: none;
  padding: 5px 10px;
  cursor: pointer;
}
</style>
```

---

## 常見問題

### Q: 資料如何備份？

```bash
# 停止服務後複製 pb_data
sudo systemctl stop pocketbase
cp -r /opt/pocketbase/pb_data /backup/pb_data_$(date +%Y%m%d)
sudo systemctl start pocketbase

# 或使用 SQLite 線上備份（不需停止服務）
sqlite3 /opt/pocketbase/pb_data/data.db ".backup /backup/data_$(date +%Y%m%d).db"
```

### Q: 如何重設管理員密碼？

```bash
./pocketbase superuser upsert admin@example.com newpassword
```

### Q: 效能瓶頸在哪？

SQLite 的寫入鎖是主要限制。如果需要高併發寫入，考慮：

- 使用佇列處理寫入請求
- 評估是否真的需要 PocketBase（可能 PostgreSQL 更適合）

### Q: 可以用在生產環境嗎？

可以，但要評估：

- 預期流量（SQLite 適合中低流量）
- 資料重要性（確保備份策略）
- 團隊維護能力

---

## 進階學習資源

- [[PocketBase快速搭建指南]] - 更多部署選項
- [[PocketBase自訂API路由]] - 深入 API 擴充
- [[PocketBase認證與權限設定]] - 完整的權限控制
- [官方文件](https://pocketbase.io/docs/)
- [GitHub 討論區](https://github.com/pocketbase/pocketbase/discussions)

---

## 結語

PocketBase 不是要取代所有後端方案，而是提供一個極簡的選擇。當你的需求是快速驗證想法、建立個人專案、或者只是需要一個簡單的後端時，它能讓你在幾分鐘內就有一個功能完整的後端服務。

這篇教學涵蓋了從入門到實戰的主要知識點。實際專案中可能還會遇到其他問題，但有了這個基礎，查閱官方文件或社群討論應該都能找到解答。

祝開發順利。
