# Convex Framework Guidelines

Generic Convex best practices for building backend applications.

---

## Function Guidelines

### New Function Syntax
ALWAYS use the new function syntax for Convex functions:
```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:
```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 registered at the exact path you specify.

### Validators

Array validator example:
```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) => {
    //...
},
});
```

Discriminated union example:
```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 `v.null()` when returning null:
```typescript
export const exampleQuery = query({
  args: {},
  returns: v.null(),
  handler: async (ctx, args) => {
      return null;
  },
});
```

### Convex Types Reference

| Convex Type | TS/JS type  | Example       | Validator                        | Notes |
|-------------|-------------|---------------|----------------------------------|-------|
| Id          | string      | `doc._id`     | `v.id(tableName)`                | |
| Null        | null        | `null`        | `v.null()`                       | Use `null` instead of `undefined` |
| Int64       | bigint      | `3n`          | `v.int64()`                      | -2^63 to 2^63-1 |
| Float64     | number      | `3.1`         | `v.number()`                     | IEEE-754 double-precision |
| Boolean     | boolean     | `true`        | `v.boolean()`                    | |
| String      | string      | `"abc"`       | `v.string()`                     | UTF-8, <1MB |
| Bytes       | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()`               | <1MB |
| Array       | Array       | `[1, 3.2]`    | `v.array(values)`                | Max 8192 values |
| Object      | Object      | `{a: "abc"}`  | `v.object({property: value})`    | Max 1024 entries |
| Record      | Record      | `{"a": "1"}`  | `v.record(keys, values)`         | ASCII keys only |

### Function Registration
- Use `internalQuery`, `internalMutation`, `internalAction` for private functions (only callable by other Convex functions)
- Use `query`, `mutation`, `action` for public API functions
- You CANNOT register functions through the `api` or `internal` objects
- ALWAYS include argument and return validators. Use `returns: v.null()` if no return value

### Function Calling
- `ctx.runQuery` - call a query from query, mutation, or action
- `ctx.runMutation` - call a mutation from mutation or action
- `ctx.runAction` - call an action from action (only for crossing runtimes V8 to Node)
- All calls take a `FunctionReference`, not the function directly
- Minimize calls from actions to queries/mutations (risk of race conditions)

When calling functions in the same file, add type annotation:
```typescript
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
- Use `api` object for public functions: `api.example.f`
- Use `internal` object for private functions: `internal.example.g`
- File-based routing: `convex/messages/access.ts` -> `api.messages.access.h`

---

## Validator Guidelines
- `v.bigint()` is deprecated - use `v.int64()` instead
- Use `v.record()` for record types. `v.map()` and `v.set()` are not supported

---

## Schema Guidelines
- Define schema in `convex/schema.ts`
- Import schema functions from `convex/server`
- System fields `_creationTime` (v.number()) and `_id` (v.id(tableName)) are automatic
- Name indexes after their fields: `["field1", "field2"]` -> `"by_field1_and_field2"`
- Index fields must be queried in order defined

---

## TypeScript Guidelines
- Use `Id<'tableName'>` from `./_generated/dataModel` for typed IDs
- Record example: `Record<Id<"users">, string>`
- Use `as const` for string literals in discriminated unions
- Always define arrays as `const array: Array<T> = [...]`
- Always define records as `const record: Record<K, V> = {...}`
- Add `@types/node` when using Node.js built-in modules

---

## Query Guidelines
- Do NOT use `filter` - define an index and use `withIndex` instead
- No `.delete()` - use `.collect()` then iterate with `ctx.db.delete(row._id)`
- Use `.unique()` for single document (throws if multiple match)
- For async iteration, use `for await (const row of query)` instead of `.collect()`

### Ordering
- Default: ascending `_creationTime`
- Use `.order('asc')` or `.order('desc')`
- Indexed queries ordered by index columns

### Full Text Search
```typescript
const messages = await ctx.db
  .query("messages")
  .withSearchIndex("search_body", (q) =>
    q.search("body", "hello hi").eq("channel", "#general"),
  )
  .take(10);
```

---

## Mutation Guidelines
- `ctx.db.replace` - fully replace document (throws if not exists)
- `ctx.db.patch` - shallow merge updates (throws if not exists)

---

## Action Guidelines
- Add `"use node";` at top of files using Node.js modules
- Actions don't have database access (`ctx.db` not available)

```typescript
import { action } from "./_generated/server";

export const exampleAction = action({
    args: {},
    returns: v.null(),
    handler: async (ctx, args) => {
        return null;
    },
});
```

---

## Pagination
```typescript
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")
        .withIndex("by_author", (q) => q.eq("author", args.author))
        .order("desc")
        .paginate(args.paginationOpts);
    },
});
```

`paginationOpts`: `{ numItems: number, cursor: string | null }`

Returns: `{ page: Doc[], isDone: boolean, continueCursor: string }`

---

## Cron Jobs
- Use `crons.interval` or `crons.cron` only (not hourly/daily/weekly helpers)
- Pass FunctionReference, not the function directly

```typescript
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();
crons.interval("job name", { hours: 2 }, internal.crons.myFunction, {});
export default crons;
```

---

## File Storage
- `ctx.storage.getUrl()` returns signed URL (null if file doesn't exist)
- Query `_storage` system table for metadata (don't use deprecated `ctx.storage.getMetadata`)

```typescript
const metadata = await ctx.db.system.get(args.fileId);
// Returns: { _id, _creationTime, contentType?, sha256, size }
```

- Store items as `Blob` objects
