Skip to content

🔨 feat(db): add push_tokens table + tasks.editor_data column#15186

Closed
sudongyuer wants to merge 2 commits into
canaryfrom
sudongyuer/addis-ababa-v1
Closed

🔨 feat(db): add push_tokens table + tasks.editor_data column#15186
sudongyuer wants to merge 2 commits into
canaryfrom
sudongyuer/addis-ababa-v1

Conversation

@sudongyuer

@sudongyuer sudongyuer commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two independent DB schema additions bundled together since both ship as schema-only changes that need to be cherry-picked into the next release/db-migration-* window:

  1. push_tokens table — data layer + mobile-facing API + send-side machinery for LobeHub Mobile remote push notifications via Expo Push Service. (LOBE-8771)
  2. tasks.editor_data columnjsonb column letting the Task instruction editor persist Lexical state (image sizes, custom nodes, etc.). Required by LOBE-8967 task attachment / image upload support (companion feature PR ✨ feat(task): support file & image attachments #15141).

Both are additive, nullable / new-table-only, and backwards compatible.

Linear: LOBE-8771, LOBE-8967
Sub-issue: LOBE-9138 — Server 端实现
Companion PRs:

Migrations

-- 0104_strange_klaw.sql (push_tokens)
CREATE TABLE IF NOT EXISTS "push_tokens" (...)
-- + FK constraints, indexes

-- 0105_add_tasks_editor_data.sql (Task rich editor)
ALTER TABLE "tasks" ADD COLUMN IF NOT EXISTS "editor_data" jsonb;

task_comments.editor_data already exists on canary (added in 0095); only tasks needed the column.

What's included

push_tokens — data layer

  • push_tokens table keyed by (user_id, device_id) so a single account can register multiple devices independently. Cascade-deletes with the user.
  • PushTokenModel with idempotent upsert (onConflictDoUpdate), scoped unregister, and a static deletePushTokensByExpoTokens helper for the receipt-cleanup worker.
  • notification_deliveries.channel widened to include 'push'.
  • NotificationSettings type gains an optional push channel.

push_tokens — tRPC

  • pushToken.register / pushToken.unregister exposed on both MobileRouter and LambdaRouter.

push_tokens — Send side (PushChannel)

  • Structurally compatible with cloud's NotificationChannel, so cloud can register it without casts.
  • Fans a single notification out to all of a user's tokens, chunks via expo-server-sdk, respects the 600 msg/sec project limit with 100ms throttle between chunks.
  • Embeds (ticketId, expoToken) pairs in providerMessageId for receipt reconciliation. (TODO marker left for promoting this to a dedicated push_tickets table if cross-delivery joins are ever needed.)

push_tokens — Receipt reconciliation (processPushReceipts)

  • Pure helper to be called by cloud's Vercel cron (see queued cloud PR).
  • Polls Expo receipts in parallel (Promise.all across chunks), updates notification_deliveries in bulk, prunes push_tokens rows flagged DeviceNotRegistered.
  • Configurable lookback window + min-age guard (default: 24h / 15min).

push_tokens — Dev tooling

  • /api/dev/test-push (development-only — 404s in production) lets you fire a real push directly to a user's registered tokens, bypassing NotificationService. Useful for end-to-end verification before cloud submodule sync.

tasks.editor_data column

Architecture (push side)

The setup uses opt-in scenarios. By default no scenario sends push — the cloud-side PR explicitly adds 'push' to image_generation_completed and video_generation_completed's channels arrays. User preferences (userSettings.notification.push) sit on top, giving users a per-channel and per-type kill switch.

Test plan

Push side:

  • 10 model tests — upsert conflict, multi-device, cascade delete, scoped unregister, deletePushTokensByExpoTokens helper
  • 7 router tests — zod validation, mutation passthrough
  • 7 PushChannel tests — no_tokens / invalid_tokens / send-time errors / rate_limited / 429 / non-429 rethrow
  • 7 processPushReceipts tests — empty pending, all-ok, mixed, error aggregation, no-yet-receipts, malformed JSON, dedupe cross-deliveries
  • type-check, lint clean
  • iOS real-device E2E — pushToken.register writes push_tokens row, dev /test-push delivers to locked iPhone via APNs

tasks.editor_data:

  • bun run db:generate regenerates a clean migration (one-line ALTER TABLE ADD COLUMN)
  • migration SQL is idempotent (ADD COLUMN IF NOT EXISTS)
  • schema snapshot + journal consistent

🤖 Generated with Claude Code

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

You don't have permission to create a Preview Deployment for this Vercel project: lobehub.

View Documentation: https://vercel.com/docs/accounts/team-members-and-roles

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sudongyuer, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. feature:api API endpoint and backend issues platform:mobile Mobile app labels May 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 993113ee38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +159 to +164
if (outcomes.every((o) => o === undefined)) continue;

const errors = outcomes.filter((o) => o?.status === 'error');

if (errors.length === 0) {
updateTasks.push(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for all ticket receipts before finalizing delivery

This logic can mark a delivery as delivered when Expo has only returned receipts for a subset of tickets: if at least one outcome is ok and the rest are still missing, errors.length === 0 triggers a final delivered update. In that case, later error receipts (for still-pending tickets) will never be reconciled because the row is no longer sent, which can hide failed pushes and skip invalid-token cleanup. This occurs when getPushNotificationReceiptsAsync returns partial results for a multi-token delivery.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.90%. Comparing base (cce1491) to head (57a04be).
⚠️ Report is 50 commits behind head on canary.

Additional details and impacted files
@@           Coverage Diff           @@
##           canary   #15186   +/-   ##
=======================================
  Coverage   70.89%   70.90%           
=======================================
  Files        3144     3145    +1     
  Lines      312964   313006   +42     
  Branches    33148    33149    +1     
=======================================
+ Hits       221881   221923   +42     
  Misses      90917    90917           
  Partials      166      166           
Flag Coverage Δ
app 61.72% <ø> (ø)
database 92.22% <100.00%> (+0.02%) ⬆️
packages/agent-runtime 80.48% <ø> (ø)
packages/builtin-tool-lobe-agent 19.87% <ø> (ø)
packages/context-engine 84.13% <ø> (ø)
packages/conversation-flow 91.28% <ø> (ø)
packages/file-loaders 87.89% <ø> (ø)
packages/memory-user-memory 74.99% <ø> (ø)
packages/model-bank 99.99% <ø> (ø)
packages/model-runtime 83.79% <ø> (ø)
packages/prompts 72.54% <ø> (ø)
packages/python-interpreter 92.90% <ø> (ø)
packages/ssrf-safe-fetch 0.00% <ø> (ø)
packages/types 35.09% <ø> (ø)
packages/utils 88.20% <ø> (ø)
packages/web-crawler 88.08% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Store 67.94% <ø> (ø)
Services 54.68% <ø> (ø)
Server 72.17% <ø> (ø)
Libs 56.42% <ø> (ø)
Utils 85.96% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Introduce the persistence layer for mobile push delivery (LOBE-8771):

- `push_tokens` table keyed by `(user_id, device_id)` with cascade
  delete and indexes for user / last-seen lookups. Lets one account
  register multiple devices independently.
- `PushTokenModel` with idempotent upsert (`onConflictDoUpdate`),
  scoped `unregister`, and a static `deletePushTokensByExpoTokens`
  helper for the receipt-cleanup worker.
- `notification_deliveries.channel` widened to `'email' | 'inbox' | 'push'`.

The send-side machinery (`PushChannel`, `processPushReceipts`, tRPC
routers, dev test endpoint) lives in a companion PR that stacks on this.

Tests: 10 model tests covering upsert conflict, multi-device, cascade
delete, scoped unregister, and `deletePushTokensByExpoTokens`.

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sudongyuer sudongyuer force-pushed the sudongyuer/addis-ababa-v1 branch from 993113e to 5d36ccd Compare May 26, 2026 03:32
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels May 26, 2026
@sudongyuer sudongyuer changed the title ✨ feat(push): add Expo push notification infrastructure 🔨 feat(db): add push_tokens table for mobile push notifications May 26, 2026
Adds the `editor_data jsonb` column on the `tasks` table so the Task
instruction editor can persist its Lexical state — image sizes, custom
nodes, and other rich attributes that markdown drops.

`task_comments.editor_data` already exists on canary; only `tasks`
needed this column.

Required by LOBE-8967.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sudongyuer sudongyuer changed the title 🔨 feat(db): add push_tokens table for mobile push notifications 🔨 feat(db): add push_tokens table + tasks.editor_data column May 26, 2026
arvinxx added a commit that referenced this pull request May 28, 2026
…eId)

This reverts commit addf14c (device-only unique index).

The device-only index conflicts with #15186's pushToken upsert, whose
onConflict target is (userId, deviceId). Restore the composite unique
index so the upsert lands consistently with both PRs.

Also re-point 0105 snapshot prevId to the restored 0104 id and carry the
(userId, deviceId) index forward so the migration chain stays consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arvinxx added a commit that referenced this pull request May 28, 2026
… document shares migrations (#15280)

* 🔨 feat(db): batch topic usage stats, push tokens, tasks editor_data & document shares

Bundle four independent schema changes onto one migration branch:

- 0104 topics: add usage/cost aggregate columns (total_cost, token totals,
  cost/usage jsonb, model, provider) + model/provider indexes
- 0105 push_tokens: new table for Expo push notification tokens
- 0106 tasks: add editor_data jsonb column
- 0107 document_shares: new table for document share flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): combine batch schema changes into a single migration

Squash the four sequential migrations (0104-0107) into one 0104 SQL file
containing all DDL: topic usage/cost columns, push_tokens table,
tasks.editor_data column, and document_shares table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): make push_tokens unique constraint device-only

Drop the userId prefix from the push_tokens unique index — one row per
device, reassigned to the new user on switch (upsert by deviceId).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(db): merge connectorTool schema into connector.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ⏪ revert(db): restore push_tokens unique constraint to (userId, deviceId)

This reverts commit addf14c (device-only unique index).

The device-only index conflicts with #15186's pushToken upsert, whose
onConflict target is (userId, deviceId). Restore the composite unique
index so the upsert lands consistently with both PRs.

Also re-point 0105 snapshot prevId to the restored 0104 id and carry the
(userId, deviceId) index forward so the migration chain stays consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(db): add devices table and consolidate batch migration into 0104

Add the `devices` identity anchor (surrogate uuid PK + unique(userId, deviceId))
as the stable, reinstall-proof base for binding agent runtime instances per
machine. Fold the prior 0104/0105 migrations and the new table into a single
idempotent 0104 migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✅ test(db): add topic usage/cost columns to topic.create assertions

The batch added 8 nullable topic columns (totalCost/usage/model/...) but
topic.create.test.ts still asserted the pre-batch 19-field shape via toEqual.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(db): use uuid primary key for document_shares

Align document_shares.id with the other new batch tables (uuid defaultRandom);
table has no consumers yet so no compat impact. Regenerated 0104 + snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: ONLY-yours <1349021570@qq.com>
@arvinxx arvinxx closed this May 29, 2026
sudongyuer added a commit that referenced this pull request May 31, 2026
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sudongyuer added a commit that referenced this pull request May 31, 2026
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sudongyuer added a commit that referenced this pull request May 31, 2026
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sudongyuer added a commit that referenced this pull request May 31, 2026
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sudongyuer added a commit that referenced this pull request May 31, 2026
…15233)

Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:api API endpoint and backend issues platform:mobile Mobile app size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants