Skip to Content
Data Layer (Convex)

Data Layer (Convex)

Convex data model and function patterns used by FluxKit.

Application Composition and Components

convex/convex.config.ts wires app-level components:

import { defineApp } from "convex/server"; import betterAuth from "./features/auth/convex.config"; import polar from "@convex-dev/polar/convex.config"; const app = defineApp(); app.use(betterAuth); app.use(polar); export default app;

This means data + auth + billing live behind a single Convex deployment boundary.

Schema design

convex/schema.ts defines core domain tables:

export default defineSchema({ tasks: defineTable({ title: v.string(), description: v.optional(v.string()), status: v.union( v.literal("pending"), v.literal("in progress"), v.literal("completed"), v.literal("cancelled"), ), priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")), category: v.string(), userId: v.string(), assignedTo: v.optional(v.string()), dueDate: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_user", ["userId"]) .index("by_status", ["status"]) .index("by_priority", ["priority"]) .index("by_category", ["category"]), });

Key strategy: each user-facing table includes either userId ownership or auth-linked identity lookups in function handlers.

Query/mutation authorization pattern

convex/tasks.ts enforces identity in every public function:

export const list = query({ args: { status: v.optional( v.union( v.literal("pending"), v.literal("in progress"), v.literal("completed"), v.literal("cancelled"), ), ), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); } let tasks = await ctx.db .query("tasks") .withIndex("by_user", (q) => q.eq("userId", identity.subject)) .collect(); if (args.status) { tasks = tasks.filter((task) => task.status === args.status); } return tasks.sort((a, b) => b.createdAt - a.createdAt); }, });

Internal workflow via scheduler

State transitions trigger async follow-up jobs. Example from convex/tasks.ts:

if (!wasCompleted && isNowCompleted) { await ctx.scheduler.runAfter(0, internal.notifications.createNotification, { userId: identity.subject, type: "task_completed", title: "Task Completed", message: `You completed the task: ${args.title ?? task.title}`, }); }

This keeps mutation latency low and isolates side effects.

HTTP surface + auth + Polar registration

convex/http.ts exposes non-Convex-client entry points:

const http = httpRouter(); authComponent.registerRoutes(http, createAuth); polar.registerRoutes(http); export default http;

Auth routes are wrapped with IP-based rate limiting for sensitive paths:

const RATE_LIMITED_PATHS = [ "/api/auth/sign-in/email", "/api/auth/sign-up/email", "/api/auth/forget-password", "/api/auth/reset-password", ];

Polar function exports

convex/polar.ts exposes billing operations directly:

export const { changeCurrentSubscription, cancelCurrentSubscription, getConfiguredProducts, listAllProducts, listAllSubscriptions, generateCheckoutLink, generateCustomerPortalUrl, } = polar.api();

These functions are the canonical data-layer interface for subscription lifecycle.

Verification commands

bunx convex codegen bun run test convex/features/email/config.test.ts bun run test convex/features/email/betterAuth.test.ts bun run typecheck
Last updated on