Skip to content

Add mobile sync API routes#1777

Open
lawrencecchen wants to merge 1 commit intomainfrom
feat/mobile-sync-routes
Open

Add mobile sync API routes#1777
lawrencecchen wants to merge 1 commit intomainfrom
feat/mobile-sync-routes

Conversation

@lawrencecchen
Copy link
Copy Markdown
Collaborator

@lawrencecchen lawrencecchen commented Apr 3, 2026

Summary

  • Add three new route files for the cmux mobile sync API: workspace mark-read, push notifications, and analytics
  • Update the existing heartbeat route to accept directConnect (for direct-connect TLS info from macOS publisher) and relax status from enum to string
  • All new routes use the same factory pattern as existing mobile routes (dependency injection for testability, dynamic imports to avoid test resolution issues)

New endpoints:

  • POST /api/mobile/workspaces/mark-read (Stack Auth bearer, calls Convex HTTP)
  • POST /api/mobile/push/register (stub, logs token)
  • POST /api/mobile/push/remove (stub, logs removal)
  • POST /api/mobile/push/test (stub, returns scheduledCount: 0)
  • POST /api/mobile/analytics (stub, logs event)

Push and analytics routes are stubs that accept and log. Full implementation (APNs, PostHog forwarding) will come later.

Test plan

  • All 12 mobile route tests pass (bunx vitest run apps/www/lib/routes/mobile-*.test.ts)
  • bun check passes (lint + typecheck)

Summary by cubic

Add mobile sync API routes for workspace read-state, push notifications, and analytics. Also update the heartbeat API to accept direct-connect TLS info and a flexible status string.

  • New Features
    • Workspace read: POST /api/mobile/workspaces/mark-read (Stack Auth bearer, forwards to Convex HTTP).
    • Push: POST /api/mobile/push/{register|remove|test} (stubs; register/remove log; test returns scheduledCount: 0).
    • Analytics: POST /api/mobile/analytics (stub; logs event).
    • Heartbeat: accepts directConnect (port, TLS pins, ticket) and status is now a string.
    • All routes use the existing factory pattern (DI, dynamic imports) for testability.

Written for commit 4548e9b. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added mobile analytics endpoint for event tracking.
    • Added mobile push notification endpoints for token management and test notifications.
    • Added mobile workspace read status tracking endpoint.
    • Enhanced mobile heartbeat reporting with direct connection configuration.
  • Tests

    • Added test coverage for mobile analytics, push notifications, and workspace operations.

Add workspace mark-read, push notification, and analytics routes.
Update heartbeat route to accept directConnect and flexible status.
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux-client Ready Ready Preview, Comment Apr 3, 2026 4:13am
cmux-www Ready Ready Preview, Comment Apr 3, 2026 4:13am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

This PR introduces three new mobile API route handlers (analytics event ingestion, push notification management, and workspace read tracking) with OpenAPI-validated request/response schemas and token-based authentication. These routers are registered in the main Hono app under the /api base path. The mobile heartbeat route is also updated to support directConnect configuration options.

Changes

Cohort / File(s) Summary
Route Registration
apps/www/lib/hono-app.ts, apps/www/lib/routes/index.ts
Imports and exports three new mobile routers; registers them in the Hono app to handle requests under /api base path.
Mobile Analytics
apps/www/lib/routes/mobile-analytics.route.ts, apps/www/lib/routes/mobile-analytics.route.test.ts
New POST /mobile/analytics endpoint with Zod/OpenAPI schemas, access-token validation, event ingest handler, and test coverage for authenticated/unauthenticated cases.
Mobile Push
apps/www/lib/routes/mobile-push.route.ts, apps/www/lib/routes/mobile-push.route.test.ts
Three POST endpoints (/mobile/push/register, /mobile/push/remove, /mobile/push/test) for push-notification management with configurable dependency injection, Zod validation, and comprehensive test suite.
Mobile Workspace Read
apps/www/lib/routes/mobile-workspace-read.route.ts, apps/www/lib/routes/mobile-workspace-read.route.test.ts
New POST /mobile/workspaces/mark-read endpoint with access-token and team verification, Convex integration for persistence, and test coverage.
Mobile Heartbeat Update
apps/www/lib/routes/mobile-heartbeat.route.ts
Extended heartbeat schema to include optional directConnect object (directPort, directTlsPins, ticketSecret) and relaxed status field from enum to free-form string.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Hops through the API with analytics in tow,
Push notifications flutter where fast signals flow,
Mark reads and heartbeats, a workspace to know,
Three new mobile routers make the platform grow! 📱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add mobile sync API routes' accurately and concisely describes the main change—adding new mobile route handlers for the sync API.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-sync-routes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 3, 2026

Greptile Summary

This PR adds three new mobile sync API route files (mark-read, push, analytics) and updates the heartbeat route to accept a directConnect field and relax status to an open string. All new routes follow the established factory/DI pattern and include tests.

  • P1 — mobile-workspace-read.route.ts: verifyTeam is called without a try/catch; if it throws (team not found or access denied), Hono returns a 500 instead of a 403/404, making the error opaque to clients.

Confidence Score: 4/5

Safe to merge after adding error handling around verifyTeam in the workspace-read route.

One P1 finding: unhandled verifyTeam exceptions in the mark-read handler produce 500s for invalid/unauthorized team access. The remaining findings are P2 (stub type annotation drift, enum relaxation). The push and analytics routes are clean stubs with good test coverage.

apps/www/lib/routes/mobile-workspace-read.route.ts — needs error handling around verifyTeam

Important Files Changed

Filename Overview
apps/www/lib/routes/mobile-workspace-read.route.ts New workspace mark-read route; verifyTeam exceptions are unhandled, causing 500s for invalid team slugs or unauthorized access instead of 403/404.
apps/www/lib/routes/mobile-analytics.route.ts New analytics stub route; minor type mismatch in default ingest stub (accessToken omitted from inner type annotation).
apps/www/lib/routes/mobile-push.route.ts New push notification stub routes (register, remove, test); all authenticated, well-structured, and consistent with the factory pattern.
apps/www/lib/routes/mobile-heartbeat.route.ts Adds directConnect field and relaxes status from enum to z.string(), weakening schema validation.
apps/www/lib/routes/mobile-workspace-read.route.test.ts Tests cover happy path and unauthenticated case; no test for verifyTeam throwing (team not found / access denied).
apps/www/lib/routes/mobile-push.route.test.ts Good coverage of all three push endpoints plus unauthenticated rejection.
apps/www/lib/routes/mobile-analytics.route.test.ts Tests cover happy path (ingest called with correct args) and unauthenticated rejection.
apps/www/lib/hono-app.ts Three new routers imported and mounted; no issues.
apps/www/lib/routes/index.ts Re-exports three new routers; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Mobile App] -->|POST /mobile/workspaces/mark-read| B[Hono API]
    B --> C{Auth check}
    C -->|invalid| D[401 Unauthorized]
    C -->|valid| E[verifyTeam]
    E -->|throws| F[500 - BUG: unhandled]
    E -->|resolves| G[Call Convex mark-read]
    G --> H[200 ok=true]

    A -->|POST /mobile/push/*| B
    B --> I{Auth check}
    I -->|invalid| D
    I -->|valid| J[Stub handler - log only]
    J --> K[200 ok=true]

    A -->|POST /mobile/analytics| B
    B --> L{Auth check}
    L -->|invalid| D
    L -->|valid| M[Stub ingest - log only]
    M --> N[200 accepted=true]

    A -->|POST /mobile/heartbeat| B
    B --> O{Verify machine JWT}
    O -->|invalid| D
    O -->|valid| P[Publish to Convex with directConnect]
    P --> Q[202 accepted=true]
Loading

Reviews (1): Last reviewed commit: "Add mobile sync API routes for iOS app a..." | Re-trigger Greptile

Copy link
Copy Markdown

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

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: 4548e9b829

ℹ️ 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".

.replace(/\/$/, "");
// Calls Convex HTTP endpoint to mark the workspace as read.
const response = await fetch(
`${endpoint}/api/mobile/workspaces/mark-read`,
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 Register Convex handler for mark-read endpoint

The default implementation now POSTs to .../api/mobile/workspaces/mark-read, but the Convex HTTP router only registers /api/mobile/heartbeat (see packages/convex/convex/http.ts), so this request path will return 404 in production. Because this branch throws on non-OK responses, every real call to POST /api/mobile/workspaces/mark-read will fail with a server error instead of marking the workspace as read.

Useful? React with 👍 / 👎.

tailscaleHostname: z.string().optional(),
tailscaleIPs: z.array(z.string()),
status: z.enum(["online", "offline", "unknown"]),
status: z.string(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce heartbeat status enum at request boundary

Changing status validation to z.string() allows arbitrary values through this API, but the downstream Convex mutation still validates status as online|offline|unknown (mobileMachines.upsertHeartbeatInternal). Any other value now passes initial validation and fails later during mutation args validation, turning a client input issue into a runtime 5xx path.

Useful? React with 👍 / 👎.

Comment on lines +128 to +133
const team = await verifyTeam({
req: c.req.raw,
accessToken,
teamSlugOrId: body.teamSlugOrId,
});

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 Unhandled verifyTeam exception causes 500

If verifyTeam throws (e.g., the team slug doesn't exist or the user doesn't have access), the error propagates uncaught and Hono returns a 500. The OpenAPI definition declares no 403/404, so clients receive an opaque server error instead of an actionable auth/not-found response. A try/catch around the call (or a null-returning signature with a guard) is needed here.

Comment on lines +32 to +40
const ingest =
options?.ingest ??
(async (args: {
event: string;
properties?: Record<string, unknown>;
}) => {
// Stub: log and accept. Forward to PostHog later.
console.log("mobile-analytics:", args.event, args.properties ?? {});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Default ingest stub type omits accessToken

The default stub's inner type annotation is { event: string; properties?: ... }, dropping accessToken. The actual call (line 77) passes accessToken — it's silently ignored by the stub. When PostHog forwarding is added later, whoever wires it in via the default path will need to remember to add accessToken to the stub signature, or will find the parameter missing. The stub should mirror the full option type to avoid this drift.

Suggested change
const ingest =
options?.ingest ??
(async (args: {
event: string;
properties?: Record<string, unknown>;
}) => {
// Stub: log and accept. Forward to PostHog later.
console.log("mobile-analytics:", args.event, args.properties ?? {});
});
const ingest =
options?.ingest ??
(async (args: {
accessToken: string;
event: string;
properties?: Record<string, unknown>;
}) => {
// Stub: log and accept. Forward to PostHog later.
console.log("mobile-analytics:", args.event, args.properties ?? {});
});

tailscaleHostname: z.string().optional(),
tailscaleIPs: z.array(z.string()),
status: z.enum(["online", "offline", "unknown"]),
status: z.string(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 status widened to unbounded z.string()

Changing from z.enum(["online", "offline", "unknown"]) to z.string() removes all schema-level validation and will generate a less descriptive OpenAPI spec. If new values are needed (e.g., "direct-connect"), consider extending the enum rather than accepting any string, so invalid values are rejected at the API boundary rather than silently forwarded to Convex.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/www/lib/routes/mobile-workspace-read.route.ts (2)

41-53: Unused accessToken parameter in default verifyTeam.

The function signature accepts accessToken but the default implementation doesn't use it—verifyTeamAccess extracts the token from the request instead. This is fine functionally but slightly misleading.

Optional: simplify to match actual usage
   const verifyTeam =
     options?.verifyTeam ??
     (async ({
       req,
       teamSlugOrId,
     }: {
       req: Request;
-      accessToken: string;
       teamSlugOrId: string;
     }) => {

Note: This would require updating the type signature at lines 19-23 as well if you want full consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/lib/routes/mobile-workspace-read.route.ts` around lines 41 - 53, The
default verifyTeam wrapper currently declares an unused accessToken parameter;
update the async function signature to remove accessToken from both the
parameter list and its type annotation so it matches actual usage (the
implementation uses req and teamSlugOrId only). Locate the wrapper that imports
verifyTeamAccess (the async block referencing verifyTeamAccess) and also update
the related type definition around the earlier signature (the types at the lines
that currently list req, accessToken, teamSlugOrId) to drop accessToken for
consistency.

81-85: Misleading field name: teamSlugOrId always receives a UUID.

args.teamId contains team.uuid (from line 136), but it's sent as teamSlugOrId. While this works (the Convex mutation handles both), the naming is confusing. Consider either:

  1. Renaming to just teamId if the Convex endpoint accepts UUIDs directly, or
  2. Adding a comment clarifying the intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/lib/routes/mobile-workspace-read.route.ts` around lines 81 - 85, The
request body field name is misleading: sending args.teamId (which is a UUID) as
teamSlugOrId creates confusion; update the payload to use a clear name by
replacing teamSlugOrId with teamId in the JSON body (the JSON.stringify call
that builds the body) so the Convex mutation receives teamId, or alternatively
add a one-line comment next to the body construction explaining that args.teamId
is a UUID and is intentionally mapped to teamSlugOrId for compatibility with the
Convex endpoint; reference the args.teamId symbol and the JSON.stringify(...)
body in mobile-workspace-read.route.ts to locate the change.
apps/www/lib/routes/mobile-analytics.route.ts (1)

6-6: Consider using z.unknown() instead of z.any() for stricter type safety.

While z.any() works for flexible analytics properties, z.unknown() provides better type safety by requiring explicit type narrowing when accessing values.

Optional improvement
-    properties: z.record(z.string(), z.any()).optional(),
+    properties: z.record(z.string(), z.unknown()).optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/lib/routes/mobile-analytics.route.ts` at line 6, Replace the liberal
use of z.any() in the route schema with z.unknown() to enforce stricter type
safety: change the schema entry "properties: z.record(z.string(),
z.any()).optional()" to use z.unknown() for value types and then update any
downstream code that reads from the "properties" record (where it's
consumed/parsed in mobile-analytics.route.ts) to explicitly narrow/validate
values (e.g., with Zod parsing, type guards, or specific schemas) before
accessing them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/www/lib/routes/mobile-heartbeat.route.ts`:
- Line 29: The route schema currently relaxes status to z.string() (see status
in mobile-heartbeat.route.ts) which can pass invalid values into the Convex
handler; change status to the strict literal union ("online" | "offline" |
"unknown") so it matches the Convex HTTP handler expectation in
packages/convex/convex/mobile_http.ts, and either remove the unused
directConnect field from the route schema or update the Convex handler
(mobile_http.ts) to extract and forward directConnect alongside other payload
fields so it is persisted rather than silently dropped.
- Around line 17-21: The Convex HTTP handler is not extracting or forwarding the
directConnect payload (HeartbeatDirectConnect/directPort, directTlsPins,
ticketSecret), so those fields are dropped; update the handler in
packages/convex/convex/mobile_http.ts to accept and validate a directConnect
object (using HeartbeatDirectConnect shape), include it in the request-body
type/validation and pass it through to the existing Convex mutation(s) that
persist heartbeats (the mutation you call to store heartbeat data), or
alternatively remove directConnect from the client schema if you choose to not
persist it yet; ensure you reference HeartbeatDirectConnect and the
directConnect property names when modifying the handler and mutation call so the
fields are forwarded and stored.

---

Nitpick comments:
In `@apps/www/lib/routes/mobile-analytics.route.ts`:
- Line 6: Replace the liberal use of z.any() in the route schema with
z.unknown() to enforce stricter type safety: change the schema entry
"properties: z.record(z.string(), z.any()).optional()" to use z.unknown() for
value types and then update any downstream code that reads from the "properties"
record (where it's consumed/parsed in mobile-analytics.route.ts) to explicitly
narrow/validate values (e.g., with Zod parsing, type guards, or specific
schemas) before accessing them.

In `@apps/www/lib/routes/mobile-workspace-read.route.ts`:
- Around line 41-53: The default verifyTeam wrapper currently declares an unused
accessToken parameter; update the async function signature to remove accessToken
from both the parameter list and its type annotation so it matches actual usage
(the implementation uses req and teamSlugOrId only). Locate the wrapper that
imports verifyTeamAccess (the async block referencing verifyTeamAccess) and also
update the related type definition around the earlier signature (the types at
the lines that currently list req, accessToken, teamSlugOrId) to drop
accessToken for consistency.
- Around line 81-85: The request body field name is misleading: sending
args.teamId (which is a UUID) as teamSlugOrId creates confusion; update the
payload to use a clear name by replacing teamSlugOrId with teamId in the JSON
body (the JSON.stringify call that builds the body) so the Convex mutation
receives teamId, or alternatively add a one-line comment next to the body
construction explaining that args.teamId is a UUID and is intentionally mapped
to teamSlugOrId for compatibility with the Convex endpoint; reference the
args.teamId symbol and the JSON.stringify(...) body in
mobile-workspace-read.route.ts to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 656b4d75-c10c-4edf-9c20-907b1a1b1c73

📥 Commits

Reviewing files that changed from the base of the PR and between 590cbad and 4548e9b.

📒 Files selected for processing (9)
  • apps/www/lib/hono-app.ts
  • apps/www/lib/routes/index.ts
  • apps/www/lib/routes/mobile-analytics.route.test.ts
  • apps/www/lib/routes/mobile-analytics.route.ts
  • apps/www/lib/routes/mobile-heartbeat.route.ts
  • apps/www/lib/routes/mobile-push.route.test.ts
  • apps/www/lib/routes/mobile-push.route.ts
  • apps/www/lib/routes/mobile-workspace-read.route.test.ts
  • apps/www/lib/routes/mobile-workspace-read.route.ts

Comment on lines +17 to +21
const HeartbeatDirectConnect = z.object({
directPort: z.number(),
directTlsPins: z.array(z.string()),
ticketSecret: z.string(),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether directConnect is handled in Convex HTTP handler
rg -n "directConnect|directPort|directTlsPins|ticketSecret" packages/convex/

Repository: manaflow-ai/manaflow

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Examine the schema file at apps/www/lib/routes/mobile-heartbeat.route.ts
echo "=== Checking schema file ==="
cat -n apps/www/lib/routes/mobile-heartbeat.route.ts | head -70

Repository: manaflow-ai/manaflow

Length of output: 2709


🏁 Script executed:

#!/bin/bash
# Check if the Convex mobile_http.ts file exists and examine it
echo "=== Checking Convex handler ==="
if [ -f "packages/convex/convex/mobile_http.ts" ]; then
  cat -n "packages/convex/convex/mobile_http.ts" | head -80
else
  # Try to find mobile_http file
  find packages/convex -name "*mobile*" -type f 2>/dev/null
fi

Repository: manaflow-ai/manaflow

Length of output: 2138


🏁 Script executed:

#!/bin/bash
# Search for where the heartbeat data is being used in the codebase
echo "=== Searching for heartbeat references ==="
rg -n "HeartbeatDirectConnect" --type ts --type tsx -i

Repository: manaflow-ai/manaflow

Length of output: 151


🏁 Script executed:

#!/bin/bash
# Check the specific lines mentioned in "Also applies to"
echo "=== Checking line 32 (directConnect in HeartbeatBody) ==="
sed -n '30,35p' apps/www/lib/routes/mobile-heartbeat.route.ts

echo "=== Checking line 61 (directConnect in function signature) ==="
sed -n '59,63p' apps/www/lib/routes/mobile-heartbeat.route.ts

echo "=== Checking line 149 ==="
sed -n '147,151p' apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 810


🏁 Script executed:

#!/bin/bash
# Check if directConnect is being sent to Convex in the publishMobileHeartbeatToConvex function
echo "=== Checking how directConnect is passed to Convex ==="
sed -n '69,90p' apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 716


🏁 Script executed:

#!/bin/bash
# Check full context around line 149
echo "=== Context around line 149 ==="
sed -n '140,160p' apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 700


🏁 Script executed:

#!/bin/bash
# Check if the Convex handler actually handles directConnect despite my initial grep
echo "=== Full Convex handler file ==="
wc -l packages/convex/convex/mobile_http.ts

Repository: manaflow-ai/manaflow

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Get more context to understand what function is being called around line 149
echo "=== Context for line 149 (function being called) ==="
sed -n '130,160p' apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 1008


🏁 Script executed:

#!/bin/bash
# Check if there's any handling of directConnect in the mutations
echo "=== Check mobileMachines.upsertHeartbeatInternal signature ==="
rg -n "upsertHeartbeatInternal" packages/convex/ -A 10

Repository: manaflow-ai/manaflow

Length of output: 1788


🏁 Script executed:

#!/bin/bash
# Check the actual mutation definition to see if it accepts directConnect
echo "=== Check full upsertHeartbeatInternal mutation ==="
sed -n '54,90p' packages/convex/convex/mobileMachines.ts

Repository: manaflow-ai/manaflow

Length of output: 1190


🏁 Script executed:

#!/bin/bash
# Check the schema of mobileMachine to see if it has directConnect fields
echo "=== Check mobileMachine schema ==="
rg -n "directConnect|directPort|directTlsPins|ticketSecret" packages/convex/convex/mobileMachines.ts

Repository: manaflow-ai/manaflow

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Verify the complete data flow - check what the Convex HTTP handler actually receives vs what it uses
echo "=== Verify Convex handler receives directConnect ==="
sed -n '4,45p' packages/convex/convex/mobile_http.ts

Repository: manaflow-ai/manaflow

Length of output: 1298


🏁 Script executed:

#!/bin/bash
# Check if there are any other handlers or recent updates
echo "=== Check all HTTP handlers in Convex ==="
find packages/convex -name "*http*" -type f

Repository: manaflow-ai/manaflow

Length of output: 768


directConnect fields are silently dropped by the Convex backend.

The client sends directConnect (with directPort, directTlsPins, ticketSecret) in the request body to the Convex HTTP endpoint, but the handler at packages/convex/convex/mobile_http.ts does not extract these fields from the request. The type annotation for the request body (lines 9-31) excludes directConnect, and the subsequent mutations (lines 33-43, 45-52) never receive or persist these fields. The data is lost.

Either:

  1. Update the Convex HTTP handler to extract and forward directConnect fields to a mutation, or
  2. Remove these fields from the schema until the backend is ready.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/lib/routes/mobile-heartbeat.route.ts` around lines 17 - 21, The
Convex HTTP handler is not extracting or forwarding the directConnect payload
(HeartbeatDirectConnect/directPort, directTlsPins, ticketSecret), so those
fields are dropped; update the handler in packages/convex/convex/mobile_http.ts
to accept and validate a directConnect object (using HeartbeatDirectConnect
shape), include it in the request-body type/validation and pass it through to
the existing Convex mutation(s) that persist heartbeats (the mutation you call
to store heartbeat data), or alternatively remove directConnect from the client
schema if you choose to not persist it yet; ensure you reference
HeartbeatDirectConnect and the directConnect property names when modifying the
handler and mutation call so the fields are forwarded and stored.

tailscaleHostname: z.string().optional(),
tailscaleIPs: z.array(z.string()),
status: z.enum(["online", "offline", "unknown"]),
status: z.string(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Convex mutation's status validator
rg -n "status:" packages/convex/convex/mobileMachines.ts -A5

Repository: manaflow-ai/manaflow

Length of output: 599


🏁 Script executed:

#!/bin/bash
# Check the mobile-heartbeat.route.ts file around line 29 and relevant schema definitions
rg -n "status.*z\." apps/www/lib/routes/mobile-heartbeat.route.ts -B2 -A2

Repository: manaflow-ai/manaflow

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Check if directConnect is in the HeartbeatBody schema and how it's used
rg -n "directConnect\|HeartbeatBody" apps/www/lib/routes/mobile-heartbeat.route.ts -B1 -A1

Repository: manaflow-ai/manaflow

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check the Convex HTTP handler to see if directConnect fields are extracted
rg -n "directConnect\|mobile_http" packages/convex/convex/mobile_http.ts -B2 -A2

Repository: manaflow-ai/manaflow

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Get the full schema definition around line 29
sed -n '15,50p' apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 1065


🏁 Script executed:

#!/bin/bash
# Search for HeartbeatDirectConnect or directConnect more broadly
rg -i "directconnect|directport|directtls" apps/www/lib/routes/mobile-heartbeat.route.ts

Repository: manaflow-ai/manaflow

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check what Convex expects in the mobile_http handler
cat packages/convex/convex/mobile_http.ts

Repository: manaflow-ai/manaflow

Length of output: 1693


Relaxing status to z.string() will cause runtime errors downstream.

The Convex HTTP handler (packages/convex/convex/mobile_http.ts) expects a strict literal union: status: "online" | "offline" | "unknown". Your route will accept any string value, but non-enum statuses will fail at the Convex layer.

Additionally, the directConnect field accepted by your route schema is never extracted by the HTTP handler and will be silently dropped without being persisted.

Suggested fix
-    status: z.string(),
+    status: z.enum(["online", "offline", "unknown"]),

To preserve the directConnect data, add extraction and forwarding in the Convex HTTP handler, or remove it from the route schema if it's not needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
status: z.string(),
status: z.enum(["online", "offline", "unknown"]),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/lib/routes/mobile-heartbeat.route.ts` at line 29, The route schema
currently relaxes status to z.string() (see status in mobile-heartbeat.route.ts)
which can pass invalid values into the Convex handler; change status to the
strict literal union ("online" | "offline" | "unknown") so it matches the Convex
HTTP handler expectation in packages/convex/convex/mobile_http.ts, and either
remove the unused directConnect field from the route schema or update the Convex
handler (mobile_http.ts) to extract and forward directConnect alongside other
payload fields so it is persisted rather than silently dropped.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

6 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/www/lib/routes/mobile-push.route.ts">

<violation number="1" location="apps/www/lib/routes/mobile-push.route.ts:80">
P1: Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.</violation>

<violation number="2" location="apps/www/lib/routes/mobile-push.route.ts:95">
P2: Avoid logging full push message content from request payloads; log metadata only.</violation>
</file>

<file name="apps/www/lib/routes/mobile-workspace-read.route.ts">

<violation number="1" location="apps/www/lib/routes/mobile-workspace-read.route.ts:52">
P2: Pass `accessToken` to `verifyTeamAccess` in the default `verifyTeam` implementation; currently the token is resolved and passed into `verifyTeam` but then dropped.</violation>

<violation number="2" location="apps/www/lib/routes/mobile-workspace-read.route.ts:74">
P1: The default `markRead` implementation POSTs to `/api/mobile/workspaces/mark-read` on the Convex site, but the Convex HTTP router (`packages/convex/convex/http.ts`) only registers `/api/mobile/heartbeat`. This will return 404 in production, and since the response is checked with `if (!response.ok)` and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.</violation>
</file>

<file name="apps/www/lib/routes/mobile-analytics.route.ts">

<violation number="1" location="apps/www/lib/routes/mobile-analytics.route.ts:39">
P2: Avoid logging raw analytics `properties`; they can contain sensitive user data and should not be emitted verbatim to server logs.</violation>
</file>

<file name="apps/www/lib/routes/mobile-heartbeat.route.ts">

<violation number="1" location="apps/www/lib/routes/mobile-heartbeat.route.ts:29">
P1: Relaxing `status` to `z.string()` removes API-boundary validation while the downstream Convex mutation (`upsertHeartbeatInternal`) still enforces `"online" | "offline" | "unknown"`. Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

platform: args.platform,
bundleId: args.bundleId,
deviceId: args.deviceId,
tokenPrefix: args.token.slice(0, 8),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P1: Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-push.route.ts, line 80:

<comment>Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.</comment>

<file context>
@@ -0,0 +1,243 @@
+        platform: args.platform,
+        bundleId: args.bundleId,
+        deviceId: args.deviceId,
+        tokenPrefix: args.token.slice(0, 8),
+      });
+    });
</file context>
Suggested change
tokenPrefix: args.token.slice(0, 8),
tokenLength: args.token.length,
Fix with Cubic

.replace(/\/$/, "");
// Calls Convex HTTP endpoint to mark the workspace as read.
const response = await fetch(
`${endpoint}/api/mobile/workspaces/mark-read`,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P1: The default markRead implementation POSTs to /api/mobile/workspaces/mark-read on the Convex site, but the Convex HTTP router (packages/convex/convex/http.ts) only registers /api/mobile/heartbeat. This will return 404 in production, and since the response is checked with if (!response.ok) and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-workspace-read.route.ts, line 74:

<comment>The default `markRead` implementation POSTs to `/api/mobile/workspaces/mark-read` on the Convex site, but the Convex HTTP router (`packages/convex/convex/http.ts`) only registers `/api/mobile/heartbeat`. This will return 404 in production, and since the response is checked with `if (!response.ok)` and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.</comment>

<file context>
@@ -0,0 +1,148 @@
+        .replace(/\/$/, "");
+      // Calls Convex HTTP endpoint to mark the workspace as read.
+      const response = await fetch(
+        `${endpoint}/api/mobile/workspaces/mark-read`,
+        {
+          method: "POST",
</file context>
Fix with Cubic

tailscaleHostname: z.string().optional(),
tailscaleIPs: z.array(z.string()),
status: z.enum(["online", "offline", "unknown"]),
status: z.string(),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P1: Relaxing status to z.string() removes API-boundary validation while the downstream Convex mutation (upsertHeartbeatInternal) still enforces "online" | "offline" | "unknown". Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-heartbeat.route.ts, line 29:

<comment>Relaxing `status` to `z.string()` removes API-boundary validation while the downstream Convex mutation (`upsertHeartbeatInternal`) still enforces `"online" | "offline" | "unknown"`. Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.</comment>

<file context>
@@ -14,15 +14,22 @@ const HeartbeatWorkspace = z.object({
     tailscaleHostname: z.string().optional(),
     tailscaleIPs: z.array(z.string()),
-    status: z.enum(["online", "offline", "unknown"]),
+    status: z.string(),
     lastSeenAt: z.number().optional(),
     lastWorkspaceSyncAt: z.number().optional(),
</file context>
Suggested change
status: z.string(),
status: z.enum(["online", "offline", "unknown"]),
Fix with Cubic

(async (args: { title: string; body: string }) => {
console.log("mobile-push: test push requested", {
title: args.title,
body: args.body,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P2: Avoid logging full push message content from request payloads; log metadata only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-push.route.ts, line 95:

<comment>Avoid logging full push message content from request payloads; log metadata only.</comment>

<file context>
@@ -0,0 +1,243 @@
+    (async (args: { title: string; body: string }) => {
+      console.log("mobile-push: test push requested", {
+        title: args.title,
+        body: args.body,
+      });
+      return 0;
</file context>
Suggested change
body: args.body,
bodyLength: args.body.length,
Fix with Cubic

const { verifyTeamAccess } = await import(
"@/lib/utils/team-verification"
);
return await verifyTeamAccess({ req, teamSlugOrId });
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P2: Pass accessToken to verifyTeamAccess in the default verifyTeam implementation; currently the token is resolved and passed into verifyTeam but then dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-workspace-read.route.ts, line 52:

<comment>Pass `accessToken` to `verifyTeamAccess` in the default `verifyTeam` implementation; currently the token is resolved and passed into `verifyTeam` but then dropped.</comment>

<file context>
@@ -0,0 +1,148 @@
+      const { verifyTeamAccess } = await import(
+        "@/lib/utils/team-verification"
+      );
+      return await verifyTeamAccess({ req, teamSlugOrId });
+    });
+  const markRead =
</file context>
Fix with Cubic

properties?: Record<string, unknown>;
}) => {
// Stub: log and accept. Forward to PostHog later.
console.log("mobile-analytics:", args.event, args.properties ?? {});
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P2: Avoid logging raw analytics properties; they can contain sensitive user data and should not be emitted verbatim to server logs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-analytics.route.ts, line 39:

<comment>Avoid logging raw analytics `properties`; they can contain sensitive user data and should not be emitted verbatim to server logs.</comment>

<file context>
@@ -0,0 +1,90 @@
+      properties?: Record<string, unknown>;
+    }) => {
+      // Stub: log and accept. Forward to PostHog later.
+      console.log("mobile-analytics:", args.event, args.properties ?? {});
+    });
+
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant