chore: prepare v1.0.0 for Netlify deployment

Update version to 1.0.0 across package.json and changelog. Configure netlify.toml with Convex deployment URL (agreeable-trout-200.convex.site). Verify TypeScript type-safety for src and convex directories. Confirm Netlify build passes with SPA 404 fallback configured. Update TASK.md with deployment steps and files.md with complete file structure.
This commit is contained in:
Wayne Sutton
2025-12-14 11:30:22 -08:00
parent 6e8d1b1138
commit 462729de58
62 changed files with 14537 additions and 9 deletions

View File

@@ -0,0 +1,637 @@
---
description: Guidelines for preventing write conflicts when using React, useEffect, and Convex
globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx
alwaysApply: true
---
# Preventing Write Conflicts in Convex with React
Write conflicts occur when two functions running in parallel make conflicting changes to the same table or document. This rule provides patterns to avoid these conflicts.
## Understanding Write Conflicts
According to [Convex documentation](https://docs.convex.dev/error#1), write conflicts happen when:
1. Multiple mutations update the same document concurrently
2. A mutation reads data that changes during execution
3. Mutations are called more rapidly than Convex can execute them
Convex uses optimistic concurrency control and will retry mutations automatically, but will eventually fail permanently if conflicts persist.
## Backend Protection: Idempotent Mutations
### Always Make Mutations Idempotent
Mutations should be safe to call multiple times with the same result.
**Good Pattern:**
```typescript
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
// Early return if document doesn't exist
if (!task) {
return null;
}
// Early return if already in desired state
if (task.status === "completed") {
return null;
}
// Only update if state change is needed
await ctx.db.patch(args.taskId, {
status: "completed",
completedAt: Date.now(),
});
return null;
},
});
```
**Bad Pattern:**
```typescript
// This will cause conflicts if called multiple times rapidly
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
// No check if already completed
await ctx.db.patch(args.taskId, {
status: "completed",
completedAt: Date.now(),
});
return null;
},
});
```
### Avoid Unnecessary Reads - Patch Directly
When you only need to update fields, patch directly without reading first. Database operations throw if the document doesn't exist.
**Good Pattern:**
```typescript
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Patch directly without reading first
// ctx.db.patch throws if document doesn't exist
await ctx.db.patch(args.id, { content: args.content });
return null;
},
});
```
**Bad Pattern:**
```typescript
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Reading the document first creates a conflict window
const note = await ctx.db.get(args.id);
if (!note) throw new Error("Not found");
// When typing rapidly, multiple mutations fire
// Each reads the same version, then all try to write, causing conflicts
await ctx.db.patch(args.id, { content: args.content });
return null;
},
});
```
### Minimize Data Reads
Only read the data you need. Avoid querying entire tables when you only need specific documents.
**Good Pattern:**
```typescript
export const updateUserCount = mutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
// Only query tasks for this specific user
const tasks = await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.collect();
await ctx.db.patch(args.userId, {
taskCount: tasks.length,
});
return null;
},
});
```
**Bad Pattern:**
```typescript
export const updateUserCount = mutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
// Reading entire table creates conflicts with any task change
const allTasks = await ctx.db.query("tasks").collect();
const userTasks = allTasks.filter((t) => t.userId === args.userId);
await ctx.db.patch(args.userId, {
taskCount: userTasks.length,
});
return null;
},
});
```
### Use Indexes to Reduce Read Scope
Always define and use indexes to limit the scope of data reads.
```typescript
// In schema.ts
tasks: defineTable({
userId: v.string(),
status: v.string(),
content: v.string(),
}).index("by_user", ["userId"])
.index("by_user_and_status", ["userId", "status"]),
```
## Frontend Protection: Preventing Duplicate Calls
### Use Refs to Track Mutation Calls
When mutations should only be called once per state change, use refs to track calls.
**Good Pattern:**
```typescript
export function TimerComponent() {
const [session, setSession] = useState(null);
const hasCalledComplete = useRef(false);
const completeSession = useMutation(api.timer.completeSession);
useEffect(() => {
if (timeRemaining <= 0 && session && !hasCalledComplete.current) {
hasCalledComplete.current = true;
completeSession({ sessionId: session._id });
}
}, [timeRemaining, session, completeSession]);
// Reset ref when starting new session
const handleStartNewSession = async () => {
hasCalledComplete.current = false;
await startSession();
};
return <div>...</div>;
}
```
**Bad Pattern:**
```typescript
export function TimerComponent() {
const [session, setSession] = useState(null);
const completeSession = useMutation(api.timer.completeSession);
useEffect(() => {
// This can be called multiple times if timeRemaining updates
if (timeRemaining <= 0 && session) {
completeSession({ sessionId: session._id });
}
}, [timeRemaining, session, completeSession]);
return <div>...</div>;
}
```
### Debounce Rapid Mutations
For user-triggered actions that can happen rapidly (typing, dragging), debounce the mutation calls. Recommended delay: 300-500ms.
**Good Pattern:**
```typescript
import { useMutation } from "convex/react";
import { useCallback } from "react";
import debounce from "lodash/debounce";
export function EditableNote() {
const updateNote = useMutation(api.notes.updateNote);
// Debounce updates to prevent conflicts during rapid typing (300-500ms)
const debouncedUpdate = useCallback(
debounce((noteId: Id<"notes">, content: string) => {
updateNote({ noteId, content });
}, 500), // 500ms delay recommended
[updateNote]
);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const content = e.target.value;
setLocalContent(content);
debouncedUpdate(noteId, content);
};
return <textarea onChange={handleChange} value={localContent} />;
}
```
### Avoid Calling Mutations in Loops
Never call mutations inside loops without proper batching or rate limiting.
**Good Pattern:**
```typescript
// Use a single mutation that handles batch updates
export const updateMultipleTasks = mutation({
args: {
updates: v.array(
v.object({
taskId: v.id("tasks"),
completed: v.boolean(),
}),
),
},
returns: v.null(),
handler: async (ctx, args) => {
for (const update of args.updates) {
const task = await ctx.db.get(update.taskId);
if (task && task.completed !== update.completed) {
await ctx.db.patch(update.taskId, {
completed: update.completed,
});
}
}
return null;
},
});
// Call once from frontend
const updates = selectedTasks.map((task) => ({
taskId: task._id,
completed: true,
}));
await updateMultipleTasks({ updates });
```
**Bad Pattern:**
```typescript
// Calling mutation in a loop
for (const task of selectedTasks) {
await updateTask({ taskId: task._id, completed: true });
}
```
### Use Parallel Updates with Promise.all
When updating multiple independent documents, use Promise.all for parallel writes instead of sequential loops.
**Good Pattern:**
```typescript
export const reorderItems = mutation({
args: { itemIds: v.array(v.id("items")) },
returns: v.null(),
handler: async (ctx, args) => {
// Patch all items in parallel
const updates = args.itemIds.map((id, index) =>
ctx.db.patch(id, { order: index }),
);
await Promise.all(updates);
return null;
},
});
```
**Bad Pattern:**
```typescript
export const reorderItems = mutation({
args: { itemIds: v.array(v.id("items")) },
returns: v.null(),
handler: async (ctx, args) => {
// Sequential reads and writes create conflict windows
for (let i = 0; i < args.itemIds.length; i++) {
const item = await ctx.db.get(args.itemIds[i]); // Read
await ctx.db.patch(args.itemIds[i], { order: i }); // Write
}
return null;
},
});
```
### Check Mutation Status Before Calling Again
Use the mutation's loading state to prevent duplicate calls.
**Good Pattern:**
```typescript
export function TaskItem() {
const [isPending, setIsPending] = useState(false);
const toggleTask = useMutation(api.tasks.toggleTask);
const handleToggle = async () => {
if (isPending) return; // Prevent duplicate calls
setIsPending(true);
try {
await toggleTask({ taskId: task._id });
} finally {
setIsPending(false);
}
};
return (
<button onClick={handleToggle} disabled={isPending}>
{task.completed ? "Completed" : "Not completed"}
</button>
);
}
```
## Schema Design to Avoid Conflicts
### Use Event Records for High-Frequency Counters
Instead of updating a counter on a document (which creates conflicts), create individual event records and aggregate them in queries.
**Good Pattern:**
```typescript
// Separate view tracking documents
export const trackView = mutation({
args: { pageId: v.id("pages") },
returns: v.null(),
handler: async (ctx, args) => {
// Create individual view records instead
await ctx.db.insert("views", {
pageId: args.pageId,
timestamp: Date.now(),
});
return null;
// Aggregate views in a query or scheduled function
},
});
// Query to get view count
export const getViewCount = query({
args: { pageId: v.id("pages") },
returns: v.number(),
handler: async (ctx, args) => {
const views = await ctx.db
.query("views")
.withIndex("by_page", (q) => q.eq("pageId", args.pageId))
.collect();
return views.length;
},
});
```
**Bad Pattern:**
```typescript
// Global counter updated by all users - creates conflicts
export const trackView = mutation({
args: { pageId: v.id("pages") },
returns: v.null(),
handler: async (ctx, args) => {
const page = await ctx.db.get(args.pageId);
if (!page) throw new Error("Not found");
// Many users updating the same document simultaneously
await ctx.db.patch(args.pageId, {
views: page.views + 1,
});
return null;
},
});
```
### Minimize Document Updates
Design your schema so frequently changing data is isolated.
**Good Pattern:**
```typescript
// Separate frequently updated data
sessions: defineTable({
userId: v.string(),
startTime: v.number(),
}).index("by_user", ["userId"]),
sessionMetrics: defineTable({
sessionId: v.id("sessions"),
remainingTime: v.number(),
status: v.string(),
lastUpdated: v.number(),
}).index("by_session", ["sessionId"]),
```
**Bad Pattern:**
```typescript
// Mixing static and frequently updated data
sessions: defineTable({
userId: v.string(),
startTime: v.number(),
// These fields update frequently and cause conflicts
remainingTime: v.number(),
status: v.string(),
lastUpdated: v.number(),
}).index("by_user", ["userId"]),
```
### Use Single User Sessions
For data like timers or active sessions, use a pattern where only one document per user exists.
```typescript
export const getActiveSession = query({
args: {},
returns: v.union(v.object({...}), v.null()),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query("sessions")
.withIndex("by_user", (q) => q.eq("userId", identity.subject))
.first();
},
});
export const startSession = mutation({
args: {},
returns: v.id("sessions"),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// Delete existing session first
const existing = await ctx.db
.query("sessions")
.withIndex("by_user", (q) => q.eq("userId", identity.subject))
.first();
if (existing) {
await ctx.db.delete(existing._id);
}
return await ctx.db.insert("sessions", {
userId: identity.subject,
startTime: Date.now(),
});
},
});
```
## Authorization Patterns
When you need to verify ownership before updates, use these patterns to minimize conflicts:
### Option A: Use Indexes for User-Scoped Queries
```typescript
export const updateNote = mutation({
args: {
id: v.id("notes"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
// Only query notes the user owns
const note = await ctx.db
.query("notes")
.withIndex("by_user", (q) => q.eq("userId", identity.subject))
.filter((q) => q.eq(q.field("_id"), args.id))
.unique();
if (!note) throw new Error("Not found");
await ctx.db.patch(args.id, { content: args.content });
return null;
},
});
```
### Option B: Internal Mutations with Schema-Level Security
```typescript
// Internal mutation with no auth check (fast, no conflicts)
export const _updateNote = internalMutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { content: args.content });
return null;
},
});
// Public mutation with auth check
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const note = await ctx.db.get(args.id);
if (!note || note.userId !== identity.subject) {
throw new Error("Not found");
}
// Call internal mutation (no read before write)
await ctx.runMutation(internal.notes._updateNote, {
id: args.id,
content: args.content,
});
return null;
},
});
```
## Monitoring Write Conflicts
Check your Convex dashboard for:
- **Insight Breakdown**: Shows which mutations are retrying due to conflicts
- **Error Logs**: Permanent failures after retries
- **Function Latency**: High latency may indicate frequent retries
Regular monitoring helps you identify and fix conflict patterns early.
## Checklist for Preventing Write Conflicts
**Backend (Convex mutations):**
- [ ] Make mutations idempotent with early returns
- [ ] Patch directly without reading first when possible
- [ ] Check if document exists before updating (only when necessary)
- [ ] Check if update is needed (current state vs desired state)
- [ ] Use indexed queries with selective filters
- [ ] Avoid reading entire tables
- [ ] Minimize the scope of data reads
- [ ] Use Promise.all for parallel independent updates
- [ ] Design schema to separate frequently updated fields
- [ ] Use event records pattern for high-frequency counters
- [ ] Consider internal mutations for auth-checked operations
**Frontend (React components):**
- [ ] Use refs to track one-time mutation calls
- [ ] Reset refs when starting new operations
- [ ] Debounce rapid user input mutations (300-500ms)
- [ ] Check mutation loading state before calling again
- [ ] Avoid calling mutations in loops
- [ ] Batch multiple updates into single mutation calls
- [ ] Use proper useEffect dependencies
## Resources
- [Convex Write Conflicts Documentation](https://docs.convex.dev/error#1)
- [Optimistic Concurrency Control](https://docs.convex.dev/database/advanced/occ)
- [Convex Best Practices](https://docs.convex.dev/understanding/best-practices/)
## When Write Conflicts Are Acceptable
Some write conflicts are expected and acceptable:
- Very high-frequency updates (gaming leaderboards, live counters)
- Genuinely concurrent user actions on the same resource
- Temporary spikes in activity
In these cases, ensure your mutations are idempotent and let Convex's retry mechanism handle the conflicts. Add user-facing retry logic if needed.
## Summary
**Key Takeaway:** The less you read before writing, the fewer conflicts you'll have. Design your mutations to write directly when possible, and structure your data model to avoid concurrent writes to the same documents.
**Priority actions:**
1. Patch directly without reading first (most common fix)
2. Use indexed queries to minimize read scope
3. Debounce rapid user inputs (300-500ms)
4. Use event records for high-frequency counters
5. Monitor your dashboard for conflict patterns

720
.cursor/rules/convex2.mdc Normal file
View File

@@ -0,0 +1,720 @@
---
description:
globs:
alwaysApply: true
---
---
description: Guidelines and best practices for building Convex projects, including database schema design, queries, mutations, and real-world examples
globs: **/\*.ts,**/_.tsx,\*\*/_.js,\*_/_.jsx
---
# Convex guidelines
## Function guidelines
### New function syntax
- always create type-safe code
- ALWAYS use the new function syntax for Convex functions. For example:
`typescript
import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
});
`
### Http endpoint syntax
- HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator. For example:
`typescript
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/echo",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
}),
});
`
- HTTP endpoints are always registered at the exact path you specify in the `path` field. For example, if you specify `/api/someRoute`, the endpoint will be registered at `/api/someRoute`.
### Validators
- Below is an example of an array validator:
```typescript
import { mutation } from "./\_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
//...
},
});
```
- Below is an example of a schema with validators that codify a discriminated union type:
```typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
results: defineTable(
v.union(
v.object({
kind: v.literal("error"),
errorMessage: v.string(),
}),
v.object({
kind: v.literal("success"),
value: v.number(),
}),
),
)
});
```
- Always use the `v.null()` validator when returning a null value. Below is an example query that returns a null value:
```typescript
import { query } from "./\_generated/server";
import { v } from "convex/values";
export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This query returns a null value");
return null;
},
});
```
- Here are the valid Convex types along with their respective validators:
Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Id | string | `doc._id` | `v.id(tableName)` | |
| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |
| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |
| Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. |
| Boolean | boolean | `true` | `v.boolean()` |
| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
| Array | Array] | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". |
### Function registration
- Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`.
- Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private.
- You CANNOT register a function through the `api` or `internal` objects.
- ALWAYS include argument and return validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()` as its output validator.
- If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns `null`.
### Function calling
- Use `ctx.runQuery` to call a query from a query, mutation, or action.
- Use `ctx.runMutation` to call a mutation from a mutation or action.
- Use `ctx.runAction` to call an action from an action.
- ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead.
- Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions.
- All of these calls take in a `FunctionReference`. Do NOT try to pass the callee function directly into one of these calls.
- When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example,
```
export const f = query({
args: { name: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
return "Hello " + args.name;
},
});
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
```
### Function references
- Function references are pointers to registered Convex functions.
- Use the `api` object defined by the framework in `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`.
- Use the `internal` object defined by the framework in `convex/_generated/api.ts` to call internal (or private) functions registered with `internalQuery`, `internalMutation`, or `internalAction`.
- Convex uses file-based routing, so a public function defined in `convex/example.ts` named `f` has a function reference of `api.example.f`.
- A private function defined in `convex/example.ts` named `g` has a function reference of `internal.example.g`.
- Functions can also registered within directories nested within the `convex/` folder. For example, a public function `h` defined in `convex/messages/access.ts` has a function reference of `api.messages.access.h`.
### Api design
- Convex uses file-based routing, so thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory.
- Use `query`, `mutation`, and `action` to define public functions.
- Use `internalQuery`, `internalMutation`, and `internalAction` to define private, internal functions.
### Pagination
- Paginated queries are queries that return a list of results in incremental pages.
- You can define pagination using the following syntax:
```ts
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";
export const listWithExtraArg = query({
args: { paginationOpts: paginationOptsValidator, author: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("author"), args.author))
.order("desc")
.paginate(args.paginationOpts);
},
});
```
Note: `paginationOpts` is an object with the following properties:
- `numItems`: the maximum number of documents to return (the validator is `v.number()`)
- `cursor`: the cursor to use to fetch the next page of documents (the validator is `v.union(v.string(), v.null())`)
- A query that ends in `.paginate()` returns an object that has the following properties: - page (contains an array of documents that you fetches) - isDone (a boolean that represents whether or not this is the last page of documents) - continueCursor (a string that represents the cursor to use to fetch the next page of documents)
## Validator guidelines
- `v.bigint()` is deprecated for representing signed 64-bit integers. Use `v.int64()` instead.
- Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not supported.
## Schema guidelines
- Always define your schema in `convex/schema.ts`.
- Always import the schema definition functions from `convex/server`:
- System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`.
- Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2".
- Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes.
## Typescript guidelines
- You can use the helper typescript type `Id` imported from './\_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use `Id<'users'>` to get the type of the id for that table.
- If you need to define a `Record` make sure that you correctly provide the type of the key and value in the type. For example a validator `v.record(v.id('users'), v.string())` would have the type `Record<Id<'users'>, string>`. Below is an example of using `Record` with an `Id` type in a query:
```ts
import { query } from "./\_generated/server";
import { Doc, Id } from "./\_generated/dataModel";
export const exampleQuery = query({
args: { userIds: v.array(v.id("users")) },
returns: v.record(v.id("users"), v.string()),
handler: async (ctx, args) => {
const idToUsername: Record<Id<"users">, string> = {};
for (const userId of args.userIds) {
const user = await ctx.db.get(userId);
if (user) {
users[user._id] = user.username;
}
}
return idToUsername;
},
});
```
- Be strict with types, particularly around id's of documents. For example, if a function takes in an id for a document in the 'users' table, take in `Id<'users'>` rather than `string`.
- Always use `as const` for string literals in discriminated union types.
- When using the `Array` type, make sure to always define your arrays as `const array: Array<T> = [...];`
- When using the `Record` type, make sure to always define your records as `const record: Record<KeyType, ValueType> = {...};`
- Always add `@types/node` to your `package.json` when using any Node.js built-in modules.
## Full text search guidelines
- A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like:
const messages = await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) =>
q.search("body", "hello hi").eq("channel", "#general"),
)
.take(10);
## Query guidelines
- Do NOT use `filter` in queries. Instead, define an index in the schema and use `withIndex` instead.
- Convex queries do NOT support `.delete()`. Instead, `.collect()` the results, iterate over them, and call `ctx.db.delete(row._id)` on each result.
- Use `.unique()` to get a single document from a query. This method will throw an error if there are multiple documents that match the query.
- When using async iteration, don't use `.collect()` or `.take(n)` on the result of a query. Instead, use the `for await (const row of query)` syntax.
### Ordering
- By default Convex always returns documents in ascending `_creationTime` order.
- You can use `.order('asc')` or `.order('desc')` to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending.
- Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans.
## Mutation guidelines
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist.
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist.
## Action guidelines
- Always add `"use node";` to the top of files containing actions that use Node.js built-in modules.
- Never use `ctx.db` inside of an action. Actions don't have access to the database.
- Below is an example of the syntax for an action:
```ts
import { action } from "./\_generated/server";
export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This action does not return anything");
return null;
},
});
```
## Scheduling guidelines
### Cron guidelines
- Only use the `crons.interval` or `crons.cron` methods to schedule cron jobs. Do NOT use the `crons.hourly`, `crons.daily`, or `crons.weekly` helpers.
- Both cron methods take in a FunctionReference. Do NOT try to pass the function directly into one of these methods.
- Define crons by declaring the top-level `crons` object, calling some methods on it, and then exporting it as default. For example,
```ts
import { cronJobs } from "convex/server";
import { internal } from "./\_generated/api";
import { internalAction } from "./\_generated/server";
const empty = internalAction({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("empty");
},
});
const crons = cronJobs();
// Run `internal.crons.empty` every two hours.
crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {});
export default crons;
```
- You can register Convex functions within `crons.ts` just like any other file.
- If a cron calls an internal function, always import the `internal` object from '\_generated/api`, even if the internal function is registered in the same file.
## File storage guidelines
- Convex includes file storage for large files like images, videos, and PDFs.
- The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns `null` if the file doesn't exist.
- Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's metadata.
Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
```
import { query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
type FileMetadata = {
_id: Id<"_storage">;
_creationTime: number;
contentType?: string;
sha256: string;
size: number;
}
export const exampleQuery = query({
args: { fileId: v.id("_storage") },
returns: v.null();
handler: async (ctx, args) => {
const metadata: FileMetadata | null = await ctx.db.system.get(args.fileId);
console.log(metadata);
return null;
},
});
```
- Convex storage stores items as `Blob` objects. You must convert all items to/from a `Blob` when using Convex storage.
# Examples:
## Example: chat-app
### Task
```
Create a real-time chat application backend with AI responses. The app should:
- Allow creating users with names
- Support multiple chat channels
- Enable users to send messages to channels
- Automatically generate AI responses to user messages
- Show recent message history
The backend should provide APIs for:
1. User management (creation)
2. Channel management (creation)
3. Message operations (sending, listing)
4. AI response generation using openai's GPT-4
Messages should be stored with their channel, author, and content. The system should maintain message order
and limit history display to the 10 most recent messages per channel.
```
### Analysis
1. Task Requirements Summary:
- Build a real-time chat backend with AI integration
- Support user creation
- Enable channel-based conversations
- Store and retrieve messages with proper ordering
- Generate AI responses automatically
2. Main Components Needed:
- Database tables: users, channels, messages
- Public APIs for user/channel management
- Message handling functions
- Internal AI response generation system
- Context loading for AI responses
3. Public API and Internal Functions Design:
Public Mutations:
- createUser:
- file path: convex/index.ts
- arguments: {name: v.string()}
- returns: v.object({userId: v.id("users")})
- purpose: Create a new user with a given name
- createChannel:
- file path: convex/index.ts
- arguments: {name: v.string()}
- returns: v.object({channelId: v.id("channels")})
- purpose: Create a new channel with a given name
- sendMessage:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels"), authorId: v.id("users"), content: v.string()}
- returns: v.null()
- purpose: Send a message to a channel and schedule a response from the AI
Public Queries:
- listMessages:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.array(v.object({
\_id: v.id("messages"),
\_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}))
- purpose: List the 10 most recent messages from a channel in descending creation order
Internal Functions:
- generateResponse:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.null()
- purpose: Generate a response from the AI for a given channel
- loadContext:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.array(v.object({
\_id: v.id("messages"),
\_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}))
- writeAgentResponse:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels"), content: v.string()}
- returns: v.null()
- purpose: Write an AI response to a given channel
4. Schema Design:
- users
- validator: { name: v.string() }
- indexes: <none>
- channels
- validator: { name: v.string() }
- indexes: <none>
- messages
- validator: { channelId: v.id("channels"), authorId: v.optional(v.id("users")), content: v.string() }
- indexes
- by_channel: ["channelId"]
5. Background Processing:
- AI response generation runs asynchronously after each user message
- Uses openai's GPT-4 to generate contextual responses
- Maintains conversation context using recent message history
### Implementation
#### package.json
```typescript
{
"name": "chat-app",
"description": "This example shows how to build a chat app without authentication.",
"version": "1.0.0",
"dependencies": {
"convex": "^1.17.4",
"openai": "^4.79.0"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}
```
#### tsconfig.json
```typescript
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"jsx": "react-jsx"
},
"exclude": ["convex"],
"include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"]
}
```
#### convex/index.ts
```typescript
import {
query,
mutation,
internalQuery,
internalMutation,
internalAction,
} from "./_generated/server";
import { v } from "convex/values";
import openai from "openai";
import { internal } from "./_generated/api";
/**
* Create a user with a given name.
*/
export const createUser = mutation({
args: {
name: v.string(),
},
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", { name: args.name });
},
});
/**
* Create a channel with a given name.
*/
export const createChannel = mutation({
args: {
name: v.string(),
},
returns: v.id("channels"),
handler: async (ctx, args) => {
return await ctx.db.insert("channels", { name: args.name });
},
});
/**
* List the 10 most recent messages from a channel in descending creation order.
*/
export const listMessages = query({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
return messages;
},
});
/**
* Send a message to a channel and schedule a response from the AI.
*/
export const sendMessage = mutation({
args: {
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const user = await ctx.db.get(args.authorId);
if (!user) {
throw new Error("User not found");
}
await ctx.db.insert("messages", {
channelId: args.channelId,
authorId: args.authorId,
content: args.content,
});
await ctx.scheduler.runAfter(0, internal.index.generateResponse, {
channelId: args.channelId,
});
return null;
},
});
const openai = new openai();
export const generateResponse = internalAction({
args: {
channelId: v.id("channels"),
},
returns: v.null(),
handler: async (ctx, args) => {
const context = await ctx.runQuery(internal.index.loadContext, {
channelId: args.channelId,
});
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: context,
});
const content = response.choices[0].message.content;
if (!content) {
throw new Error("No content in response");
}
await ctx.runMutation(internal.index.writeAgentResponse, {
channelId: args.channelId,
content,
});
return null;
},
});
export const loadContext = internalQuery({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
const result = [];
for (const message of messages) {
if (message.authorId) {
const user = await ctx.db.get(message.authorId);
if (!user) {
throw new Error("User not found");
}
result.push({
role: "user" as const,
content: `${user.name}: ${message.content}`,
});
} else {
result.push({ role: "assistant" as const, content: message.content });
}
}
return result;
},
});
export const writeAgentResponse = internalMutation({
args: {
channelId: v.id("channels"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("messages", {
channelId: args.channelId,
content: args.content,
});
return null;
},
});
```
#### convex/schema.ts
```typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
channels: defineTable({
name: v.string(),
}),
users: defineTable({
name: v.string(),
}),
messages: defineTable({
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}).index("by_channel", ["channelId"]),
});
```
#### src/App.tsx
```typescript
export default function App() {
return <div>Hello World</div>;
}
```

81
.cursor/rules/dev2.mdc Normal file
View File

@@ -0,0 +1,81 @@
---
description:
globs:
alwaysApply: true
---
---
description: full-stack AI convex developer
globs:
alwaysApply: true
---
- Start by saying, "let's cook
- always create type-safe code
- **!IMPORTANT**: **DO NOT** externalize or document your work, usage guidelines, or benchmarks (e.g. `README.md`, `CONTRIBUTING.md`, `SUMMARY.md`, `USAGE_GUIDELINES.md` after completing the task, unless explicitly instructed to do so. You may include a brief summary of your work, but do not create separate documentation files for it.
- When creating Convex mutations, always patch directly without reading first, use indexed queries for ownership checks instead of `ctx.db.get()`, make mutations idempotent with early returns, use timestamp-based ordering for new items, and use `Promise.all()` for parallel independent operations to avoid write conflicts.
- - When a task touches changelog.md, the changelog page, or files.md, run git log --date=short (or check commit history) and set each release date to match the real commit timeline—no placeholders or future months.
-Do you understand, what Im asking? Never assume anything on your own, if anything isnt clear, please ask questions and clarify your doubts.
- reference @dev2.mdc @help.mdc @files.md if needed
- do not use use emoji or emojis in the readme or app unless instructed
- always create type-safe code
- you understand when to use Effect and when not to use Effect https://react.dev/learn/you-might-not-need-an-effect
- you follow react docs https://react.dev/learn
- Be casual unless otherwise specified
- you are a super experienced full-stack and AI developer super experienced in React, Vite, Bun, Clerk, workos, Resend, TypeScript, and Convex.dev
- Youre an experienced AI developer with deep expertise in convex.dev, OpenAI, and Claude, following best practices for building AI powered and full-stack SaaS applications, react applications and social network platforms.
- follow the vercel Web Interface Guidelines
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/refs/heads/main/AGENTS.md and https://vercel.com/design/guidelines
- you follow convex best practices here: https://docs.convex.dev/understanding/best-practices/typescript
- you always make sure the code follows Convex typescript https://docs.convex.dev/understanding/best-practices/typescript
-
- you follow Convex dev flow https://docs.convex.dev/understanding/workflow
- you use always use Convex Queries https://docs.convex.dev/functions/query-functions
- you are an expert on convex auth functions https://docs.convex.dev/auth/functions-auth
- you use convex Mutations https://docs.convex.dev/functions/mutation-functions
- you use convex search https://docs.convex.dev/search/vector-search
- you are an expert in clerk https://clerk.com/docs/react/reference/components/authentication/sign-in
- you an an expert in convex vector search https://docs.convex.dev/search/vector-search
- you are an expert in understanding how Uploading and Storing Files with convex https://docs.convex.dev/file-storage/upload-files
- For any pop-ups, alerts, modals, warnings, notifications, or confirmations, or button confirmations, always follow the sites existing design system. Never use the browsers default pop-ups.
- Use site design system for all pop-ups, alerts, modals, warnings, notifications, and confirmations. Do not use browser defaults.
- you add short comments to your code that explains what the section does
- Be terse
-For all designs I ask you to make, have them be beautiful, not cookie cutter and never use purple or emojis unless instructed.
- Make webpages that are fully featured and worthy for production.
- Suggest solutions that I didnt think about—anticipate my needs
- Treat me as an new developer
- Be accurate and thorough
- Keep a list of the codebase files, provide a brief description of what each file one does called files.md.
- you keep a developer friendly changelog.md of new features added based on https://keepachangelog.com/en/1.0.0/
- prd files always end with .MD and not .prd
- prd files are located in the prds folder except forchangelog.M , files.MD, README.md and TASK.MDwhich can stay in the root folder
- create type-safe code always, if the prds folder does not exist create one
- Give the answer immediately. Provide detailed explanations and restate my query in your own words if necessary after giving the answer
- Value good arguments over authorities, the source is irrelevant
- Consider new technologies and contrarian ideas, not just the conventional wisdom
- You may use high levels of speculation or prediction, just flag it for me
- No moral lectures
- Discuss safety only when it's crucial and non-obvious
- If your content policy is an issue, provide the closest acceptable response and explain the content policy issue afterward
- Cite sources whenever possible at the end, not inline
- No need to mention your knowledge cutoff
- No need to disclose you're an AI
- Make code precise, modular, testable and
- never break existing functionality
- create type-safe code always
- Please respect my prettier preferences when you provide code.
- Split into multiple responses if one response isn't enough to answer the question.
- If I ask for adjustments or fix or say fix the code I have provided you, do not repeat all of my code unnecessarily. Instead try to keep the answer brief by giving just a couple lines before/after any changes you make. Multiple code blocks are ok.
- do not over engineer the code and always make the code typesafe
- do not do more than what the user ask for unless it related to fixing, adding, or updating the code to what the user is asking for
- If any changes to existing code are required, they should be minimal and focused solely on enabling the new features or resolving specific bugs with out breaking existing features.
- you never use placeholder text or images in code because everything is realtime sync with convex database
- you don't ship code with placeholder text or images
- **!IMPORTANT**: **DO NOT** externalize or document your work, usage guidelines, or benchmarks (e.g. `README.md`, `CONTRIBUTING.md`, `SUMMARY.md`, `USAGE_GUIDELINES.md` after completing the task, unless explicitly instructed to do so. You may include a brief summary of your work, but do not create separate documentation files for it.

85
.cursor/rules/help.mdc Normal file
View File

@@ -0,0 +1,85 @@
---
description: full-stack AI convex developer
globs:
alwaysApply: true
---
# Core Development Guidelines
## 1. Reflect Deeply Before Acting
Before implementing any solution, follow this reflection process:
• Carefully reflect on why the current implementation or response may not be working.
• Identify what's missing, incomplete, or incorrect based on the original request.
• Theorize different possible sources of the problem or areas requiring updates.
• Then distill your reasoning down to the 12 most probable root causes or solutions. Only proceed after clear understanding.
## 2. When Implementing Solutions
### Follow Convex's recommended approaches at all times:
• Use direct mutation calls with plain objects.
• Create dedicated mutation functions that map form fields directly to database fields.
• Ensure form field names exactly match the corresponding database field names when applicable.
### Use Convex documentation:
• Mutation Functions: https://docs.convex.dev/functions/mutation-functions
• Query Functions: https://docs.convex.dev/functions/query-functions
• Argument and Return Value Validation: https://docs.convex.dev/functions/validation
• General Function Docs: https://docs.convex.dev/functions
• When creating Convex mutations, always patch directly without reading first, use indexed queries for ownership checks instead of `ctx.db.get()`, make mutations idempotent with early returns, use timestamp-based ordering for new items, and use `Promise.all()` for parallel independent operations to avoid write conflicts.
• When a task touches changelog.md, the changelog page, or files.md, run git log --date=short (or check commit history) and set each release date to match the real commit timeline—no placeholders or future months.
• Do you understand, what Im asking? Never assume anything on your own, if anything isnt clear, please ask questions and clarify your doubts.
• reference @dev2.mdc @help.mdc @files.md if needed
### Understand the following foundational principles:
• Zen of Convex: https://docs.convex.dev/understanding/zen
• End-to-End Type Support with TypeScript: https://docs.convex.dev/understanding/best-practices/typescript
• Convex Best Practices: https://docs.convex.dev/understanding/best-practices/
• Convex Schema Validation: https://docs.convex.dev/database/schemas
## 3. Change Scope and Restrictions
When making changes to the codebase:
• Update Convex Schema if needed
• Only update files when it's directly necessary to fix the original request.
• Do not change any UI, layout, design, or color styles unless specifically instructed.
• Preserve all current admin dashboard sections and frontend components unless explicitly told to update, fix, or replace them.
• Never remove sections, features, or components unless directly requested.
## 4. UI/UX Guidelines
### Pop-ups, Alerts, Modals, Warnings, Notifications, and Confirmations:
• For any pop-ups, alerts, modals, warnings, notifications, or confirmations, always follow the site's existing design system. Never use the browser's default pop-ups.
• Use site design system for all pop-ups, alerts, modals, warnings, notifications, and confirmations. Do not use browser defaults.
### Follow the Vercel Web Interface Guidelines:
• https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/refs/heads/main/AGENTS.md
## 5. Documentation Policy
**!IMPORTANT**: **DO NOT** externalize or document your work, usage guidelines, or benchmarks (e.g. `README.md`, `CONTRIBUTING.md`, `SUMMARY.md`, `USAGE_GUIDELINES.md` after completing the task, unless explicitly instructed to do so. You may include a brief summary of your work, but do not create separate documentation files for it.
### Additional Formatting Rules:
• Never use emoji or emojis in the readme or app unless instructed
## 6. Code Confidence Requirement
Don't write any code until you're very confident (98% or more) in what needs to be done.
If unclear, ask for more info.

View File

@@ -0,0 +1,45 @@
---
description:
globs:
alwaysApply: true
---
---
description: Convex guidelines
globs:
alwaysApply: true
---
## Function guidelines
- always follow Convex schemas best practices - https://docs.convex.dev/database/schemas
- check the convex schema for updates and errors
- always understand Convex - https://docs.convex.dev/understanding/
- understand environment-variables https://docs.convex.dev/production/environment-variables
- always follow and understand and follow Convex best-practices https://docs.convex.dev/understanding/best-practices/
- Follow best practices https://docs.convex.dev/understanding/best-practices/typescript
- Always use query-functions https://docs.convex.dev/functions/query-functions
- Always usehttps://docs.convex.dev/functions/mutation-functions
- expert https://docs.convex.dev/functions/mutation-functions
- Always use https://docs.convex.dev/functions/actions
- knows https://docs.convex.dev/functions/validation
- knows https://docs.convex.dev/functions
- Expert in Clerk https://docs.clerk.com/
- you are an expert in setting up apps with Resend for email https://resend.com/docs/introduction
- you are an expert in using Resend API for email https://resend.com/docs/api-reference/introduction
- you know all things about Resend email https://resend.com/docs/knowledge-base/introduction
- expert in convex auth https://docs.convex.dev/auth/convex-auth
- convex auth docs https://labs.convex.dev/auth
- For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
- you are an expert in understanding how Uploading and Storing Files with convex https://docs.convex.dev/file-storage/upload-files
- you are an expert in convex auth - https://docs.convex.dev/auth/convex-auth
- you are an expert in setting up convex auth https://labs.convex.dev/auth/setup
- you an an expert in convex vector search https://docs.convex.dev/search/vector-search
getting-started
- do not use emoji in the readme or app
- do not over engineer the code but make it type-safe
- do not do more than what the user ask for unless it related to fixing, adding, or updating the code to what the user is asking for
waynesutton@WS-Convex merge2 %

286
.cursor/rules/sec-check.mdc Normal file
View File

@@ -0,0 +1,286 @@
# Security Guidelines for markdown-blog
This document covers security patterns specific to the markdown-blog application, a Convex-powered blog with no authentication.
## App-Specific Security Context
### Architecture
- **Frontend**: Vite + React 18.2 SPA (client-side only)
- **Backend**: Convex.dev (serverless database and functions)
- **Hosting**: Netlify with edge functions
- **Auth**: None (public blog)
### React Server Components Vulnerabilities
**Status: NOT AFFECTED**
This app does NOT use React Server Components and is NOT affected by:
- CVE-2025-55182 (Remote Code Execution)
- CVE-2025-55184 (Denial of Service)
- CVE-2025-55183 (Source Code Exposure)
These vulnerabilities affect apps using:
- `react-server-dom-webpack`
- `react-server-dom-parcel`
- `react-server-dom-turbopack`
This app uses standard React 18.2.0 client-side rendering with Vite bundler.
For the latest information, see:
- https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
- https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
## Database Tables
| Table | Contains PII | Public Access | Notes |
| ------------ | ------------ | ------------- | ---------------------------------- |
| `posts` | No | Read-only | Blog content |
| `viewCounts` | No | Write via API | View counter per post |
| `siteConfig` | No | Internal | Site settings (not currently used) |
## 1. Public API Security
### Query Functions (Read-Only)
All queries in this app are intentionally public for blog content:
```typescript
// Public queries - safe for public access
export const getAllPosts = query({...}); // List published posts
export const getPostBySlug = query({...}); // Get single post
export const getViewCount = query({...}); // Get view count
```
### Mutation Functions
| Function | Risk Level | Notes |
| -------------------- | ---------- | -------------------------------- |
| `syncPostsPublic` | Medium | Build-time sync, no auth |
| `incrementViewCount` | Low | No rate limiting, but low impact |
### syncPostsPublic Security Consideration
The `syncPostsPublic` mutation allows syncing posts without authentication. This is intentional for build-time deployment but has security implications:
```typescript
// Current: No auth check
export const syncPostsPublic = mutation({
args: { posts: v.array(...) },
handler: async (ctx, args) => {
// Syncs posts directly
},
});
```
**Mitigations in place:**
1. Mutation only affects the `posts` table
2. Posts require specific schema (slug, title, content, etc.)
3. Build-time sync uses environment variables
**Recommendations:**
- Consider adding CONVEX_DEPLOY_KEY check for production
- Monitor for unusual sync activity in Convex dashboard
## 2. HTTP Endpoint Security
### XSS Prevention
All HTTP endpoints properly escape output:
```typescript
// HTML escaping for Open Graph
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// XML escaping for RSS feeds
function escapeXml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
```
### CORS Headers
API endpoints include CORS headers for public access:
```typescript
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "public, max-age=300, s-maxage=600",
"Access-Control-Allow-Origin": "*",
}
```
This is intentional for a public blog API.
### HTTP Endpoints
| Route | Method | Auth | Description |
| --------------- | ------ | ---- | -------------------------------- |
| `/rss.xml` | GET | No | RSS feed (descriptions) |
| `/rss-full.xml` | GET | No | Full RSS feed (content for LLMs) |
| `/sitemap.xml` | GET | No | XML sitemap for SEO |
| `/api/posts` | GET | No | JSON post list |
| `/api/post` | GET | No | Single post JSON/markdown |
| `/meta/post` | GET | No | Open Graph HTML for crawlers |
## 3. Edge Function Security
### Bot Detection (botMeta.ts)
The edge function detects social media crawlers and serves Open Graph metadata:
```typescript
// Bot user agent detection
const BOTS = [
"facebookexternalhit",
"twitterbot",
// ... more bots
];
```
**Security considerations:**
- User agent can be spoofed, but this only affects OG metadata delivery
- Fallback to SPA for non-bots is secure
- No sensitive data exposed to bots
## 4. Client-Side Security
### Markdown Rendering
Uses `react-markdown` with controlled components:
- External links open with `rel="noopener noreferrer"`
- Images use lazy loading
- No raw HTML injection (markdown only)
### Copy to Clipboard
The CopyPageDropdown component uses `navigator.clipboard.writeText()` which requires user interaction and is secure.
## 5. Build-Time Security
### Environment Variables
| Variable | Purpose | Required |
| ----------------- | --------------------- | -------- |
| `VITE_CONVEX_URL` | Convex deployment URL | Yes |
| `CONVEX_URL` | Fallback Convex URL | No |
| `SITE_URL` | Canonical site URL | No |
### Sync Script
The `sync-posts.ts` script:
- Runs at build time only
- Reads markdown files from `content/blog/`
- Validates frontmatter before syncing
- Uses ConvexHttpClient with environment URL
## 6. Content Security
### Frontmatter Validation
Posts require valid frontmatter:
```typescript
// Required fields
if (!frontmatter.title || !frontmatter.date || !frontmatter.slug) {
console.warn(`Skipping ${filePath}: missing required frontmatter fields`);
return null;
}
```
### Published Flag
Only posts with `published: true` are returned by queries:
```typescript
const post = await ctx.db
.query("posts")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
if (!post || !post.published) {
return null;
}
```
## 7. Security Checklist
### Before Deploying
- [ ] Verify `VITE_CONVEX_URL` is set correctly
- [ ] Check no sensitive data in markdown files
- [ ] Review any new HTTP endpoints for proper escaping
- [ ] Ensure all external links use `noopener noreferrer`
### Convex Functions
- [ ] All queries use `.withIndex()` instead of `.filter()`
- [ ] Return validators defined for all functions
- [ ] No sensitive data in return values
### HTTP Endpoints
- [ ] HTML output uses `escapeHtml()`
- [ ] XML output uses `escapeXml()` or CDATA
- [ ] Proper Content-Type headers set
- [ ] Cache-Control headers appropriate
## 8. Known Design Decisions
### Intentionally Public
- All blog content is public by design
- No user authentication required
- API endpoints are open for LLM/agent access
- RSS feeds include full content
### Rate Limiting
- No rate limiting on view count increments
- Convex has built-in rate limiting at infrastructure level
- Consider adding application-level limits if abuse occurs
## 9. Monitoring
### Convex Dashboard
Monitor for:
- Unusual mutation activity on `syncPostsPublic`
- High query volumes on API endpoints
- Error rates on HTTP endpoints
### Netlify Analytics
Monitor for:
- Edge function errors
- Unusual traffic patterns
- Bot traffic volume
## 10. Resources
- [Convex Security Best Practices](https://docs.convex.dev/understanding/best-practices)
- [React Security Guidelines](https://react.dev/learn/security)
- [Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/)
- [React Server Components CVE](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components)

111
.cursor/rules/task.mdc Normal file
View File

@@ -0,0 +1,111 @@
---
description: Guidelines for creating and managing task lists in markdown files to track project progress
globs:
alwaysApply: false
---
---
# Task List Management
Guidelines for creating and managing task lists in markdown files to track project progress
## Task List Creation
1. Create task lists in a markdown file (in the project root):
- Use `TASKS.md` or a descriptive name relevant to the feature (e.g., `ASSISTANT_CHAT.md`)
- Include a clear title and description of the feature being implemented
2. Structure the file with these sections:
```markdown
# Feature Name Implementation
Brief description of the feature and its purpose.
## Completed Tasks
- [x] Task 1 that has been completed
- [x] Task 2 that has been completed
## In Progress Tasks
- [ ] Task 3 currently being worked on
- [ ] Task 4 to be completed soon
## Future Tasks
- [ ] Task 5 planned for future implementation
- [ ] Task 6 planned for future implementation
## Implementation Plan
Detailed description of how the feature will be implemented.
### Relevant Files
- path/to/file1.ts - Description of purpose
- path/to/file2.ts - Description of purpose
```
## Task List Maintenance
1. Update the task list as you progress:
- Mark tasks as completed by changing `[ ]` to `[x]`
- Add new tasks as they are identified
- Move tasks between sections as appropriate
2. Keep "Relevant Files" section updated with:
- File paths that have been created or modified
- Brief descriptions of each file's purpose
- Status indicators (e.g., ✅) for completed components
3. Add implementation details:
- Architecture decisions
- Data flow descriptions
- Technical components needed
- Environment configuration
## AI Instructions
When working with task lists, the AI should:
1. Regularly update the task list file after implementing significant components
2. Mark completed tasks with [x] when finished
3. Add new tasks discovered during implementation
4. Maintain the "Relevant Files" section with accurate file paths and descriptions
5. Document implementation details, especially for complex features
6. When implementing tasks one by one, first check which task to implement next
7. After implementing a task, update the file to reflect progress
## Example Task Update
When updating a task from "In Progress" to "Completed":
```markdown
## In Progress Tasks
- [ ] Implement database schema
- [ ] Create API endpoints for data access
## Completed Tasks
- [x] Set up project structure
- [x] Configure environment variables
```
Should become:
```markdown
## In Progress Tasks
- [ ] Create API endpoints for data access
## Completed Tasks
- [x] Set up project structure
- [x] Configure environment variables
- [x] Implement database schema
```
**!IMPORTANT**: **DO NOT** externalize or document your work, usage guidelines, or benchmarks (e.g. `README.md`, `CONTRIBUTING.md`, `SUMMARY.md`, `USAGE_GUIDELINES.md` after completing the task, do not use emoji, unless explicitly instructed to do so. You may include a brief summary of your work, but do not create separate documentation files for it.