Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
49e7638
Update Airtable alignment status to in progress and enhance CSV expor…
kooksee Jul 3, 2026
204d04d
Enhance documentation and UI components for Colanode. Update shipped …
kooksee Jul 7, 2026
e1ce23b
Implement cross-origin isolation headers in Vite configuration, enhan…
kooksee Jul 7, 2026
2f30704
Enhance Vite configuration with HMR settings, implement cleanup for s…
kooksee Jul 7, 2026
1cb24fb
Enhance default Page meta type by adding fields: Status, Category, Ta…
kooksee Jul 7, 2026
a18c795
Implement upgrade functionality for default Page meta type schema and…
kooksee Jul 7, 2026
d727fce
Add local workspace member management for LOCAL_ONLY mode.
kooksee Jul 8, 2026
50052f8
Harden web Docker packaging for remote deployment.
kooksee Jul 8, 2026
7a58b81
Implement server synchronization features and enhance workspace manag…
kooksee Jul 9, 2026
e4c0873
Enhance server synchronization by implementing version checks and upd…
kooksee Jul 9, 2026
a6ccde3
Refactor sync version handling and improve WebSocket validation.
kooksee Jul 9, 2026
07d79b0
Refactor WebSocket subscription handling and improve error management…
kooksee Jul 9, 2026
47305ae
Document server sync release boundaries and rollout steps.
kooksee Jul 9, 2026
676a654
Prepare server sync web MVP release notes.
kooksee Jul 9, 2026
44df6bc
Speed up web Docker CI and harden release workflows.
kooksee Jul 10, 2026
e476460
Fix Cloudflare deploy workflow secret checks.
kooksee Jul 10, 2026
7f0202e
Skip Cloudflare beta deploy when secrets are missing.
kooksee Jul 10, 2026
4c6d1c0
Make docker latest tagging work before merging to main.
kooksee Jul 10, 2026
93f3fc9
Add manual dispatch for docker latest tagging.
kooksee Jul 10, 2026
3b05d1f
Prepare feat/airtable-phase-a for merge: hygiene, lint, and meta type…
kooksee Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/skills/colanode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ description: >-
| Assets library | `settings/assets` | `ASSET_LIBRARY_SYSTEM_KIND`, `META_TYPES_SPACE_MARKER` |
| Notifications | `settings/notifications` + sidebar bell | `notification.*` mutations |

See `docs/page-meta-types.md`, `docs/todo-list.md`, `docs/asset-library.md`. Database Airtable alignment plan: `docs/database-airtable-alignment.md`.
See `docs/page-meta-types.md`, `docs/todo-list.md`, `docs/local-members.md`, `docs/form-view.md`, `docs/asset-library.md`. Database Airtable alignment plan: `docs/database-airtable-alignment.md`.

## Adding a client operation

Expand Down
3 changes: 3 additions & 0 deletions .cursor/skills/colanode/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ Mutations change user data — not exposed via MCP server (read-only MVP).
|-----|-------|
| `docs/page-meta-types.md` | Page meta type schemas |
| `docs/todo-list.md` | TodoList system task database |
| `docs/form-view.md` | Database Form view (field types, limits) |
| `docs/database-airtable-alignment.md` | Airtable alignment plan (Phase A–C) |
| `docs/database-airtable-test-plan.md` | Manual QA for Airtable features |
| `docs/asset-library.md` | Global Assets + folder filters |
| `docs/local-sync.md` | Local sync |
| `docs/tags-implementation-guide.md` | Workspace tags (**feat/sync_local only**; not on Colapp `main`) |
Expand Down
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
**/node_modules
dist
**/dist
.turbo
.git
.github
*.log
coverage
**/coverage
.DS_Store
20 changes: 19 additions & 1 deletion .github/workflows/web-cf-build-and-deploy-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,29 @@ jobs:
working-directory: apps/web
run: npm run build

- name: Check Cloudflare configuration
id: cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_WORKER_NAME_BETA: ${{ secrets.CLOUDFLARE_WORKER_NAME_BETA }}
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ] && [ -n "$CLOUDFLARE_ACCOUNT_ID" ] && [ -n "$CLOUDFLARE_WORKER_NAME_BETA" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "Cloudflare beta deploy skipped: CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_WORKER_NAME_BETA is not fully configured."
fi

- name: Create wrangler.jsonc
if: steps.cloudflare.outputs.configured == 'true'
working-directory: apps/web
env:
CLOUDFLARE_WORKER_NAME_BETA: ${{ secrets.CLOUDFLARE_WORKER_NAME_BETA }}
run: |
cat > wrangler.jsonc << EOF
{
"name": "${{ secrets.CLOUDFLARE_WORKER_NAME_BETA }}",
"name": "${CLOUDFLARE_WORKER_NAME_BETA}",
"compatibility_date": "2025-05-28",
"assets": {
"directory": "./dist",
Expand All @@ -72,6 +89,7 @@ jobs:
EOF

- name: Deploy to Cloudflare
if: steps.cloudflare.outputs.configured == 'true'
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: ./apps/web
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/web-cf-build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,29 @@ jobs:
working-directory: apps/web
run: npm run build

- name: Check Cloudflare configuration
id: cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_WORKER_NAME: ${{ secrets.CLOUDFLARE_WORKER_NAME }}
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ] && [ -n "$CLOUDFLARE_ACCOUNT_ID" ] && [ -n "$CLOUDFLARE_WORKER_NAME" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "Cloudflare deploy skipped: CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_WORKER_NAME is not fully configured."
fi

- name: Create wrangler.jsonc
if: steps.cloudflare.outputs.configured == 'true'
working-directory: apps/web
env:
CLOUDFLARE_WORKER_NAME: ${{ secrets.CLOUDFLARE_WORKER_NAME }}
run: |
cat > wrangler.jsonc << EOF
{
"name": "${{ secrets.CLOUDFLARE_WORKER_NAME }}",
"name": "${CLOUDFLARE_WORKER_NAME}",
"compatibility_date": "2025-05-28",
"assets": {
"directory": "./dist",
Expand All @@ -71,6 +88,7 @@ jobs:
EOF

- name: Deploy to Cloudflare
if: steps.cloudflare.outputs.configured == 'true'
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: ./apps/web
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/web-docker-build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

env:
REGISTRY: ghcr.io
DOCKER_PLATFORMS: linux/amd64,linux/arm64

jobs:
build:
Expand Down Expand Up @@ -51,7 +52,9 @@ jobs:
context: .
file: ./apps/web/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
platforms: ${{ env.DOCKER_PLATFORMS }}
cache-from: type=gha,scope=web-docker
cache-to: type=gha,mode=max,scope=web-docker
tags: |
${{ env.REGISTRY }}/${{ vars.WEB_IMAGE_NAME }}:${{ env.VERSION }}
labels: |
Expand Down
39 changes: 35 additions & 4 deletions .github/workflows/web-docker-mark-latest.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
name: Web - Mark docker image as latest

on:
release:
types: [published]
workflow_run:
workflows: ['Web - Build and push docker image']
types: [completed]
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Image version tag (without v prefix)'
required: true
type: string

env:
REGISTRY: ghcr.io

jobs:
tag-latest:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest

steps:
Expand All @@ -23,8 +34,28 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
REF="${{ github.event.workflow_run.head_branch }}"
echo "VERSION=${REF#v}" >> $GITHUB_ENV
else
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
fi

- name: Tag as latest the version image
run: |
docker buildx imagetools create --tag ${{ env.REGISTRY }}/${{ vars.WEB_IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ vars.WEB_IMAGE_NAME }}:${{ env.VERSION }}
IMAGE="${{ env.REGISTRY }}/${{ vars.WEB_IMAGE_NAME }}:${{ env.VERSION }}"
for attempt in $(seq 1 12); do
if docker buildx imagetools inspect "$IMAGE" >/dev/null 2>&1; then
docker buildx imagetools create \
--tag ${{ env.REGISTRY }}/${{ vars.WEB_IMAGE_NAME }}:latest \
"$IMAGE"
exit 0
fi
echo "Waiting for $IMAGE (attempt $attempt/12)..."
sleep 10
done
echo "Timed out waiting for $IMAGE"
exit 1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ apps/mobile/assets/fonts/satoshi-variable-italic.woff2
apps/mobile/assets/fonts/antonio.ttf

apps/server/data/
apps/server/.data/

.expo
web-build/
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Colanode is a local-first collaboration workspace. Clients use local SQLite stor

## Planning & feature docs
- Product roadmap and candidate directions: `docs/roadmap.md`
- Shipped features: `docs/page-meta-types.md`, `docs/todo-list.md`, `docs/local-sync.md`, `docs/asset-library.md`, etc.
- Shipped features: `docs/page-meta-types.md`, `docs/todo-list.md`, `docs/local-sync.md`, `docs/local-members.md`, `docs/asset-library.md`, `docs/form-view.md`, etc.
- Planned: `docs/database-airtable-alignment.md` (database / 多维表格 → Airtable). Workspace tags: see `docs/tags-implementation-guide.md` (**not on this branch**; `feat/sync_local` only).

## AI agents (MCP + Skills)
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,5 +1135,6 @@ ipcMain.handle(
manifest
);
await workspace.cloudSync?.importRemote();
return { userId: workspace.userId };
}
);
4 changes: 4 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ contextBridge.exposeInMainWorld('colanode', {
return ipcRenderer.invoke('local-sync-join-workspace', params);
},

localSyncDeleteRemoteWorkspace: async () => {
throw new Error('Remote workspace delete is only available on web for now');
},

onNotificationNavigate: (callback) => {
const handler = (_: unknown, target: NotificationNavigateTarget) =>
callback(target);
Expand Down
36 changes: 36 additions & 0 deletions apps/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM node:22-bookworm-slim AS build

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ \
&& rm -rf /var/lib/apt/lists/*

COPY package.json package-lock.json ./
COPY apps/server/package.json apps/server/package.json

RUN npm ci -w @colanode/server --include-workspace-root

COPY apps/server apps/server
COPY tsconfig.base.json ./

RUN npm run build -w @colanode/server && npm prune --production

FROM node:22-bookworm-slim

WORKDIR /app

ENV NODE_ENV=production
ENV PORT=4400
ENV SYNC_DB_PATH=/data/server.sqlite
ENV SYNC_STORAGE_LOCAL_ROOT=/data/storage

COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/server/package.json ./package.json
COPY --from=build /app/apps/server/dist ./dist

RUN mkdir -p /data/storage

EXPOSE 4400

CMD ["node", "dist/index.js"]
71 changes: 71 additions & 0 deletions apps/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Colanode Server (sync API)

Self-hosted sync API for unified storage. Uses local SQLite for metadata and a pluggable object store for file blobs.

## Quick start

```bash
cd apps/server
npm run dev
```

Defaults:

- API: `http://localhost:4400`
- SQLite DB: `./.data/server.sqlite`
- Object storage: `./.data/storage` (local filesystem)

## Environment

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `4400` | HTTP port |
| `SYNC_DB_PATH` | `./.data/server.sqlite` | SQLite database path |
| `SYNC_STORAGE_TYPE` | `local` | `local` or `s3` |
| `SYNC_STORAGE_LOCAL_ROOT` | `./.data/storage` | Local object storage root |

## API

- `GET /healthz`
- `GET /v1/sync/workspaces`
- `PUT /v1/sync/workspaces/:workspaceId/manifest`
- `POST /v1/sync/push`
- `GET /v1/sync/pull`
- `POST /v1/files/upload`
- `GET /v1/files/download`

## Docker

```bash
# From repository root
docker compose up --build -d
```

See [docs/server-sync.md](../../docs/server-sync.md) for full self-hosted setup.

## Data layout

- **SQLite**: sync batches, sync record index, file metadata
- **Object storage**: file blobs (`workspaces/<wsId>/files/...`)

Restarting the server preserves sync history because metadata is stored in SQLite.

## Realtime sync

Clients can subscribe to `GET /v1/sync/ws?workspaceId=...&deviceId=...` (WebSocket). When another device pushes new op-log records, the server broadcasts:

```json
{ "type": "sync.updated", "workspaceId": "...", "fromDeviceId": "...", "watermarkSeq": 123 }
```

The web client uses this to pull immediately instead of waiting for the 60s fallback poll. Local edits trigger an export within ~500ms.

## Web client (server sync mode)

Set in `apps/web/.env.local`:

```bash
VITE_SERVER_SYNC_URL=http://localhost:4400
```

Then run web + server. The web worker will push/pull workspace op-log through the sync API.
28 changes: 28 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@colanode/server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"compile": "tsc --noEmit",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.896.0",
"@aws-sdk/s3-request-presigner": "^3.896.0",
"@fastify/websocket": "^11.3.0",
"better-sqlite3": "^12.11.1",
"fastify": "^5.6.1",
"zod": "^4.1.11"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/ws": "^8.18.1",
"tsx": "^4.21.0",
"vitest": "^4.1.2"
}
}
Loading
Loading