#edit a schema object

1 messages · Page 1 of 1 (latest)

woven raven
#

how can i edit user schema and add anotther field like isLoved:boolean?
tables: {
user: TableDefinition<VObject<{
image?: string | null | undefined;
userId?: string | null | undefined;
name: string;
email: string;
emailVerified: boolean;
createdAt: number;
updatedAt: number;
}}

earnest condorBOT
#

Hi @woven raven. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

To add a new boolean field like isLoved to your user schema, you edit convex/schema.ts and update the table definition.

From the docs, schemas are defined with defineSchema, defineTable, and validators from v.[Validators]

Given your current shape, it likely looks something like:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  user: defineTable({
    image: v.optional(v.string()),
    userId: v.optional(v.string()),
    name: v.string(),
    email: v.string(),
    emailVerified: v.boolean(),
    createdAt: v.number(),
    updatedAt: v.number(),
  }),
});

To add isLoved: boolean, you just add a field:

export default defineSchema({
  user: defineTable({
    image: v.optional(v.string()),
    userId: v.optional(v.string()),
    name: v.string(),
    email: v.string(),
    emailVerified: v.boolean(),
    createdAt: v.number(),
    updatedAt: v.number(),
    isLoved: v.boolean(),          // new required field
  }),
});

If you already have data in the table, follow the migration pattern: first add it as optional, backfill, then make it required.[Intro migrations; Stateful migrations]

// Step 1: make it optional so existing docs validate
export default defineSchema({
  user: defineTable({
    image: v.optional(v.string()),
    userId: v.optional(v.string()),
    name: v.string(),
    email: v.string(),
    emailVerified: v.boolean(),
    createdAt: v.number(),
    updatedAt: v.number(),
    isLoved: v.optional(v.boolean()),
  }),
});
#

Then run a migration (e.g. using the migrations component) to set a default value on all existing users, and finally change it to isLoved: v.boolean() once all documents have the field.[Migrations component; Data migrations]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i want to edit the user table like extending it? like user:{...user, isLoved:v.boolean()}

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You can’t literally write user: { ...user, isLoved: v.boolean() } in defineSchema, but you can achieve the same effect in two common ways:

1. Just edit the table inline (simplest)

In convex/schema.ts, add the field directly to the users table definition.[Schemas; Validators]

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    // existing fields...
    name: v.string(),
    email: v.string(),
    emailVerified: v.boolean(),
    createdAt: v.number(),
    updatedAt: v.number(),

    // new field
    isLoved: v.boolean(),
  }),
});

If you already have data, add it as optional first and then migrate:[Stateful migrations; Intro migrations]

isLoved: v.optional(v.boolean()),

Run a migration to backfill, then change to v.boolean().

2. “Extend” an object validator

If you have a shared v.object you want to reuse, you can use .extend on it.[VObject.extend]

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

const baseUser = v.object({
  name: v.string(),
  email: v.string(),
  emailVerified: v.boolean(),
  createdAt: v.number(),
  updatedAt: v.number(),
});

export default defineSchema({
  users: defineTable(
    baseUser.extend({
      isLoved: v.boolean(),
    }),
  ),
});

This is the closest to “extending” the user schema.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i dont have extends since is not a a object but a table

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re right that defineTable({ ... }) takes a plain object, not a v.object, so you don’t see .extend there. The “extending” trick only works if you first define an object validator and then pass that into defineTable, which is supported.[defineTable; Validators reuse]

Here’s how to refactor so you can extend:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

// 1. Define a reusable object validator for the user shape
const userValidator = v.object({
  image: v.optional(v.string()),
  userId: v.optional(v.string()),
  name: v.string(),
  email: v.string(),
  emailVerified: v.boolean(),
  createdAt: v.number(),
  updatedAt: v.number(),
});

// 2. Extend it with your new field and use that in defineTable
export default defineSchema({
  user: defineTable(
    userValidator.extend({
      isLoved: v.boolean(), // or v.optional(v.boolean()) during migration
    }),
  ),
});

This is the Convex way to “extend” a table schema: define an object validator, call .extend on it, and pass the result to defineTable instead of using the inline { ... } form.[Validators reuse; Validators]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i followed the the sveltekit tutorial to put better auth and mostly work exept for this
const currentUserResponse = useQuery(api.auth.getCurrentUser, () => (isAuthenticated ? {} : 'skip'));
this query cant find im logged and it give me null
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
console.log(await ctx.auth.getUserIdentity())
return authComponent.getAuthUser(ctx);
},
});

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t show this exact SvelteKit + Better Auth issue, but they do describe the same pattern and common failure modes, so I’ll focus on those. [Better-auth SvelteKit usage; Auth debug]

Key points to check:


1. Make sure you’re using Convex’s auth state, not Better Auth’s

In the SvelteKit guide, isAuthenticated comes from useAuth() (the Better Auth wrapper that is wired into Convex via the plugin): [Better-auth SvelteKit usage]

import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";

const auth = useAuth();
const isAuthenticated = $derived(auth.isAuthenticated);

const currentUserResponse = useQuery(
  api.auth.getCurrentUser,
  () => (isAuthenticated ? {} : "skip")
);

If you’re instead using Better Auth’s own useSession() / getSession() or some other flag, Convex may still see you as unauthenticated. The docs explicitly say to rely on Convex’s auth state when gating Convex queries. [Better-auth authorization]


2. Confirm the Convex client is wired to Better Auth

The SvelteKit setup requires:

// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/svelte";
import { convexClient } from "@convex-dev/better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [convexClient()],
});
#
import { createSvelteAuthClient } from "@mmailaender/convex-better-auth-svelte/svelte";
import { authClient } from "$lib/auth-client";

createSvelteAuthClient({
  authClient,
  // optionally: options: { expectAuth: true },
});

If this wiring is missing or misconfigured, Convex won’t receive the Better Auth token, and ctx.auth.getUserIdentity() will log null. [Auth debug]


3. Check what ctx.auth.getUserIdentity() logs

You already log:

console.log(await ctx.auth.getUserIdentity());
return authComponent.getAuthUser(ctx);

Use the Convex dashboard logs to see the output. [Auth debug]

  • If you see null, Convex is not getting a token:

    • The Better Auth → Convex plugin path is likely misconfigured (step 2).
    • Or the query is running before Convex has validated the token (step 4).
  • If you see a non-null identity, but authComponent.getAuthUser(ctx) still returns null or throws, that’s a Better Auth side issue (e.g. session not found / not linked).


4. Ensure the query only runs after Convex is authenticated

Even if the user is signed in with Better Auth, Convex needs a moment to validate the token. The docs warn that calling authenticated queries too early will see null identity. [Better-auth authorization; Auth debug]

Your pattern:

const currentUserResponse = useQuery(
  api.auth.getCurrentUser,
  () => (isAuthenticated ? {} : "skip")
);
#

is correct if isAuthenticated is coming from Convex’s integrated auth state (via useAuth() as in step 1). If you’re using some other flag, the query may run before Convex is ready.


5. Avoid expectAuth while debugging

If you enabled expectAuth: true on the Convex client, all queries are paused until Convex thinks you’re authenticated. This can hide what’s going wrong. The Discord thread suggests turning it off while debugging. [Better-auth SvelteKit authenticated requests; Discord Better Auth + Convex]


6. Verify environment variables and Convex auth config

From the SvelteKit guide: [Better-auth SvelteKit installation]

  • BETTER_AUTH_SECRET set in Convex env.
  • SITE_URL set in Convex env.
  • PUBLIC_CONVEX_URL, PUBLIC_CONVEX_SITE_URL, PUBLIC_SITE_URL in .env.local.
  • src/convex/auth.config.ts uses getAuthConfigProvider().

If any of these are wrong, Convex may reject the token and keep you unauthenticated.


If you paste:

  • How you create authClient
  • How you call createSvelteAuthClient in Svelte
  • The actual log output of ctx.auth.getUserIdentity()

I can map it more directly to the patterns in the docs and narrow down which step is failing.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor in better auuth+convex how can i make a mutation thath eliminate an account?

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

There isn’t an explicit “delete account” example in the Better Auth + Convex docs, but you can compose it from the documented pieces:

  1. Use a Convex mutation.
  2. Inside it, get the current Better Auth user via the component client.
  3. Delete any app-side data you want.
  4. Call a Better Auth API method that deletes the user (e.g. auth.api.deleteUser), using authComponent.getAuth to get auth and headers.[Component client; Basic usage auth.api]

A mutation skeleton looks like this:

// convex/authAccount.ts
import { mutation } from "./_generated/server";
import { authComponent } from "./auth"; // your createClient(...) export
import { createAuth } from "./auth";    // your betterAuth(...) factory

export const deleteMyAccount = mutation({
  args: {},
  handler: async (ctx) => {
    // 1. Get Better Auth instance + headers for current session
    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);

    // 2. Get the current Better Auth user
    const authUser = await authComponent.getAnyUserById(
      ctx,
      (await auth.getSession({ headers }))!.user.id
    );
    if (!authUser) {
      throw new Error("No auth user found");
    }

    // 3. Delete any app-side data tied to this user (example)
    // e.g. delete from your own "users" table by authId
    const appUser = await ctx.db
      .query("users")
      .withIndex("authId", (q) => q.eq("authId", authUser._id))
      .unique();
    if (appUser) {
      await ctx.db.delete(appUser._id);
    }

    // 4. Delete the Better Auth user (API call)
    await auth.api.deleteUser({
      body: { userId: authUser._id },
      headers,
    });
  },
});

Notes:

#
  • authComponent.getAuth(createAuth, ctx) is the documented way to get auth plus the correct headers for server-side Better Auth API calls.[Component client getAuth; Basic usage auth.api]
  • The exact auth.api.deleteUser signature isn’t shown in the Convex docs; you’ll need to match it to the Better Auth docs for your version. The pattern of calling auth.api.*({ body, headers }) is what’s documented.[Basic usage auth.api]

If you’re instead using triggers to mirror Better Auth users into an app users table, you can also put your app-side cleanup into the user.onDelete trigger so it runs transactionally whenever a Better Auth user is deleted.[Triggers; Update user triggers]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i do not have access to the users table since better auth is inside a component

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re right that the Better Auth user table lives inside a component, so you can’t access it directly from your app schema. You have two main options, both supported by the Better Auth component client.


1. Delete the Better Auth user via auth.api (no direct table access)

You don’t need direct table access to delete the account; you can call Better Auth’s server API from a Convex mutation using the component client’s getAuth helper. This is the same pattern shown for changePassword in the docs.[Better-auth basic; Component client getAuth]

// convex/deleteAccount.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { authComponent, createAuth } from "./auth"; // your Better Auth wiring

export const deleteMyAccount = mutation({
  args: {},
  handler: async (ctx) => {
    // Get Better Auth instance + headers for current session
    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);

    // Call the appropriate Better Auth API method to delete the user.
    // The exact method name/signature comes from Better Auth docs,
    // but the pattern is always:
    //   auth.api.someMethod({ body: {...}, headers })
    //
    // Example shape (you must match it to your Better Auth version):
    await auth.api.deleteUser({
      body: {}, // or { userId: ... } depending on Better Auth
      headers,
    });
  },
});

Key points:

  • authComponent.getAuth(createAuth, ctx) is the documented way to get auth and headers for server-side Better Auth API calls.[Component client getAuth]
  • You never touch the component’s user table directly; Better Auth handles that internally.
#

You’ll need to look up the exact auth.api.deleteUser signature in the Better Auth docs; the Convex docs only show the pattern, not that specific method.


2. If you need app-side user data: mirror via triggers

If you want to also delete data in your own app tables, the recommended pattern is to mirror the Better Auth user into an app users table using triggers, then clean up that app data when the Better Auth user is deleted.[Triggers; User data mirroring]

Example trigger setup (simplified):

// convex/auth.ts
import { components, internal } from "./_generated/api";
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import type { DataModel } from "./_generated/dataModel";

const authFunctions: AuthFunctions = internal.auth;

export const authComponent = createClient<DataModel>(components.betterAuth, {
  authFunctions,
  triggers: {
    user: {
      onCreate: async (ctx, authUser) => {
        await ctx.db.insert("users", {
          email: authUser.email,
          authId: authUser._id, // reference to component user
        });
      },
      onDelete: async (ctx, authUser) => {
        const user = await ctx.db
          .query("users")
          .withIndex("authId", (q) => q.eq("authId", authUser._id))
          .unique();
        if (user) {
          await ctx.db.delete(user._id);
        }
      },
    },
  },
});

export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi();

Then your deleteMyAccount mutation (option 1) only needs to delete the Better Auth user; the trigger will clean up your app’s users table in the same transaction.[Triggers]


#

So even though you can’t touch the component’s user table directly, you can:

  • Delete the account via auth.api.* using authComponent.getAuth, and
  • Use triggers to keep your own app tables in sync if needed.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i also want to use the trigger but it give me an error:
export const authComponent = createClient<DataModel,typeof authSchema>(components.betterAuth,
{
triggers: {
user: {
onCreate: async (ctx, authUser) => {
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id, // reference to component user
});
},

},

},

local: {
  schema: authSchema,
},

}
Argument of type '{ triggers: { user: { onCreate: <Ctx extends GenericMutationCtx<{ pubblicita: { document: { _id: Id<"pubblicita">; _creationTime: number; image: Id<"_storage">; altImage: string; title: string; description: string; isActive: boolean; }; fieldPaths: "_id" | ExtractFieldPaths<...>; indexes: { ...; }; searchIndexes: {}...' is not assignable to parameter of type '({ local?: { schema?: SchemaDefinition<{ user: TableDefinition<VObject<{ image?: string | null | undefined; userId?: string | null | undefined; permissions?: string[] | undefined; name: string; email: string; emailVerified: boolean; createdAt: number; updatedAt: number; }, { ...; }, "required", "image" | ... 6 more ...'.
Type '{ triggers: { user: { onCreate: <Ctx extends GenericMutationCtx<{ pubblicita: { document: { _id: Id<"pubblicita">; _creationTime: number; image: Id<"_storage">; altImage: string; title: string; description: string; isActive: boolean; }; fieldPaths: "_id" | ExtractFieldPaths<...>; indexes: { ...; }; searchIndexes: {}...' is not assignable to type '{ local?: { schema?: SchemaDefinition<{ user: TableDefinition<VObject<{ image?: string | null | undefined; userId?: string | null | undefined; permissions?: string[] | undefined; name: string; email: string; emailVerified: boolean; createdAt: number; updatedAt: number; }, { ...; }, "required", "image" | ... 6 more ....'.
Property 'authFunctions' is missing in type '{ triggers: { user: { onCrea

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re very close—TypeScript is telling you exactly what’s missing: the authFunctions option. For Better Auth triggers to type-check, you must pass authFunctions into createClient, and (for Local Install) also the local.schema. [Migrate to 0.8; Triggers feature; Local install]

From the docs, the correct pattern (adapted to your Local Install) is:

// convex/auth.ts
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import authSchema from "./betterAuth/schema"; // generated by Better Auth CLI

const authFunctions: AuthFunctions = internal.auth;

export const authComponent = createClient<DataModel, typeof authSchema>(
  components.betterAuth,
  {
    authFunctions,              // <-- REQUIRED
    local: {
      schema: authSchema,       // <-- Local Install schema
    },
    triggers: {
      user: {
        onCreate: async (ctx, authUser) => {
          await ctx.db.insert("users", {
            email: authUser.email,
            authId: authUser._id,
          });
        },
        // you can add onUpdate / onDelete here too
      },
    },
  }
);

// Optional: exported trigger helpers
export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi();

Key points from the docs:

#
  • createClient takes an options object where authFunctions is required when you use triggers. [Migrate to 0.8]
  • Triggers are configured under triggers: { user: { onCreate, onUpdate, onDelete } }. [Triggers feature]
  • With Local Install, you must also pass local: { schema: authSchema } and use typeof authSchema as the second generic. [Local install]

Your error goes away once authFunctions is included alongside triggers and local.schema in that options object.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i cant deletethe account since there are no headers, how can i delete the account?
triggers: {
user: {
onCreate: async (ctx, registeredUser) => {
console.log("hi")
const user = await ctx.db
.query('user')
.withIndex('by_email', (q) => q.eq('email', registeredUser.email))
.first();
console.log(user)
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
console.log(auth)
console.log(headers)

            //if (!user) {
            //    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
            //    auth.api.deleteUser({
            //        body: {},
            //        headers: headers
            //    });
            //    return;
            //}
            //await ctx.db.patch(user._id, {
            //    id: registeredUser._id,
            //    name: user.name || registeredUser.name
            //});
        }
    }
earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You can’t use authComponent.getAuth(createAuth, ctx) (and therefore auth.api.* / headers) inside triggers. Triggers only get a Convex ctx, not the HTTP request, so there are no cookies/headers to derive a Better Auth session from. The docs only show getAuth being used inside queries/mutations/actions, not triggers.[Component client; Triggers]

So:

  • Triggers are for DB-side mirroring/cleanup only (e.g. syncing your own user table when Better Auth’s user changes).[Triggers]
  • Account deletion must be initiated from a Convex function that has headers, via authComponent.getAuth(createAuth, ctx).

How to delete the account

  1. Use a Convex mutation that runs in response to a client call (or server action), not a trigger.
  2. Inside that mutation, call authComponent.getAuth(createAuth, ctx) and then the Better Auth API to delete the user.

Example pattern (you’ll need to match the exact auth.api method name/signature from Better Auth’s docs):

// convex/account.ts
import { mutation } from "./_generated/server";
import { authComponent, createAuth } from "./auth";

export const deleteMyAccount = mutation({
  args: {},
  handler: async (ctx) => {
    // This has headers because it’s called from the client with cookies
    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);

    // Optionally: clean up your own app data here using ctx.db

    // Delete the Better Auth user (method name/signature from Better Auth docs)
    await auth.api.deleteUser({
      body: {}, // or { userId: ... } depending on your Better Auth version
      headers,
    });
  },
});
#

Then call deleteMyAccount from the client after the user confirms deletion.

What triggers are for

Your trigger should not try to call auth.api.deleteUser. Instead, use it to keep your own tables in sync when Better Auth changes its own data, for example:

triggers: {
  user: {
    onCreate: async (ctx, registeredUser) => {
      // mirror into your own "user" table, etc.
    },
    onDelete: async (ctx, registeredUser) => {
      // delete from your own "user" table, etc.
    },
  },
},

The actual Better Auth account deletion should be done via a mutation as above, not from the trigger.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor i need to delete the account o creation cause i dont want to accept it

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re running into a limitation of how Better Auth + Convex are wired: triggers don’t have headers, so you cannot call authComponent.getAuth(createAuth, ctx) or any auth.api.* method from inside a trigger. Triggers are only for DB-side mirroring/cleanup. [Triggers]

So you cannot “reject” a user by calling auth.api.deleteUser from user.onCreate.

You have two realistic options, depending on what “don’t want to accept it” means for your app:


1. Soft‑reject in your own tables (recommended)

Let Better Auth create the user, but immediately mark them as rejected / inactive in your app’s users table via the trigger. You then treat them as “non‑users” everywhere.

Example trigger:

export const authComponent = createClient<DataModel, typeof authSchema>(
  components.betterAuth,
  {
    authFunctions,
    local: { schema: authSchema },
    triggers: {
      user: {
        onCreate: async (ctx, authUser) => {
          // Mirror into your own table with a "rejected" flag
          await ctx.db.insert("users", {
            authId: authUser._id,
            email: authUser.email,
            status: "rejected", // or isAccepted: false
          });
        },
      },
    },
  }
);

Then in your app logic, always check status / isAccepted and block access if they’re rejected. This is exactly the kind of thing triggers are meant for: transactional sync between Better Auth and your own tables. [Triggers; User mirroring]


2. Defer actual deletion to a mutation

#

If you truly must delete the Better Auth user (not just mark them rejected), you need to do it from a Convex function that has headers, not from the trigger. [Component client getAuth; Basic usage auth.api]

A common pattern:

  1. In user.onCreate, store enough info in your own table to know this user is “pending review”.
  2. After your review logic (maybe in a background job or admin UI), call a mutation that:
    • Uses authComponent.getAuth(createAuth, ctx) to get { auth, headers }.
    • Calls the appropriate auth.api.* method to delete that Better Auth user.
    • Cleans up your own tables.

Example mutation skeleton (you must match the exact Better Auth API from their docs; Convex docs only show the pattern):

export const rejectUser = mutation({
  args: { authId: v.id("user") },
  handler: async (ctx, args) => {
    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);

    // Delete Better Auth user – method name/signature from Better Auth docs
    await auth.api.deleteUser({
      body: { userId: args.authId }, // or whatever your version expects
      headers,
    });

    // Clean up your own app data
    const appUser = await ctx.db
      .query("users")
      .withIndex("authId", (q) => q.eq("authId", args.authId))
      .unique();
    if (appUser) {
      await ctx.db.delete(appUser._id);
    }
  },
});

You cannot move this logic into the trigger because triggers never see headers. [Triggers]


Summary

#
  • Triggers: use them to mirror/sync your own tables (e.g. mark user as rejected, delete app data on onDelete). They cannot call auth.api.* because there are no headers. [Triggers]
  • Actual Better Auth account deletion: must be done in a Convex query/mutation/action that has headers via authComponent.getAuth(createAuth, ctx). [Component client getAuth; Basic usage auth.api]

Given that constraint, the practical solution for “don’t want to accept it on creation” is to soft‑reject in your own tables in onCreate, and only hard‑delete via a separate mutation if you really need to.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor what is the best way to handle this query?
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id('image'))
},
handler: async (ctx, { images }) => {
ctx.db.query("image").withIndex("by_id",(q)=>q.eq("_id",))
//return images.map((id) => ctx.storage.getUrl(id));
}
});

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You don’t need a DB query here at all; you already have the storage IDs in images, so just map them to URLs with ctx.storage.getUrl and Promise.all. That’s exactly how the docs show serving files from storage.[Serve files; StorageReader.getUrl]

import { internalQuery } from "./_generated/server";
import { v } from "convex/values";

export const getAllUrl = internalQuery({
  args: {
    images: v.array(v.id("_storage")), // or your own type if you wrap it
  },
  handler: async (ctx, { images }) => {
    return Promise.all(
      images.map((id) => ctx.storage.getUrl(id))
    );
  },
});

If your images arg is actually IDs from an image table (not storage IDs), then:

  1. Fetch the docs by ID (or via an index if you have one).
  2. Extract the storage ID field.
  3. Call ctx.storage.getUrl on those.

But if images is already Id<"_storage">, the direct getUrl approach above is the simplest and most efficient.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor why i cant do somthing like this:
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id('image'))
},
handler: async (ctx, { images }) => {
const allImages = await Promise.all(images.map((id) => ctx.db.get(id)));
return Promise.all(allImages.map((img) => ctx.storage.getUrl(img!.image)));
}
});

export const getAll = query({
args: {},
handler: async (ctx) => {
const pubblicita = await ctx.db
.query('pubblicita')
.withIndex('by_deleted', (q) => q.eq('deleted', false))
.collect();

    const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
        images: pubblicita.map((p) => p.image)
    });

    return [pubblicita,imagesUrl]
}

});
getAll give me an error

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re very close; the pattern you’re using is valid in Convex, but there are two common gotchas that can cause getAll to fail:

  1. Type mismatch on images argument
  2. Nulls from db.get or from storage.getUrl

From the knowledge sources:

  • db.get takes a table name and an id: ctx.db.get("tasks", args.id).[Reading data]
  • storage.getUrl takes an Id<"_storage"> and returns Promise<null | string>.[StorageReader.getUrl]
  • Internal queries are called from actions/mutations via ctx.runQuery(internal.someFn, args).[Internal functions]

Let’s walk through your code and tighten it up.

1. Make sure images is the right id type

Your internal query:

export const getAllUrl = internalQuery({
  args: {
    images: v.array(v.id('image'))
  },
  handler: async (ctx, { images }) => {
    const allImages = await Promise.all(images.map((id) => ctx.db.get(id)));
    return Promise.all(allImages.map((img) => ctx.storage.getUrl(img!.image)));
  }
});

Issues to check:

  • v.id('image') means each element is Id<"image">, i.e. a document id from the image table.
  • ctx.db.get must be called as ctx.db.get("image", id), not ctx.db.get(id). [Reading data]

So the handler should be:

export const getAllUrl = internalQuery({
  args: {
    images: v.array(v.id("image")),
  },
  handler: async (ctx, { images }) => {
    const allImages = await Promise.all(
      images.map((id) => ctx.db.get("image", id))
    );
#

// allImages: (Doc<"image"> | null)[]
return Promise.all(
allImages.map((img) =>
img ? ctx.storage.getUrl(img.image) : Promise.resolve(null)
)
);
},
});


Note the null‑check: `db.get` can return `null` if a document doesn’t exist.[[Reading data](<https://docs.convex.dev/database/reading-data>)]

Also ensure that `img.image` is actually an `Id<"_storage">` in your schema; `storage.getUrl` expects that type.[[Serve files](<https://docs.convex.dev/file-storage/serve-files#generating-file-urls-in-queries>); [StorageReader.getUrl](<https://docs.convex.dev/api/interfaces/server.StorageReader#geturl>)]

### 2. Calling the internal query

Your public query:

```ts
export const getAll = query({
  args: {},
  handler: async (ctx) => {
    const pubblicita = await ctx.db
      .query('pubblicita')
      .withIndex('by_deleted', (q) => q.eq('deleted', false))
      .collect();

    const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
      images: pubblicita.map((p) => p.image)
    });

    return [pubblicita, imagesUrl];
  }
});

This is structurally fine, assuming:

  • p.image is of type Id<"image"> (matching v.id("image") in getAllUrl).
  • You imported internal from ./_generated/api and the path internal.image.query.getAllUrl matches your file/export name.[Internal functions]

If p.image is actually an Id<"_storage">, then your internal query should instead accept v.id("_storage") and skip the db.get entirely:

export const getAllUrl = internalQuery({
  args: {
    images: v.array(v.id("_storage")),
  },
  handler: async (ctx, { images }) => {
    return Promise.all(images.map((id) => ctx.storage.getUrl(id)));
  },
});

Then images: pubblicita.map((p) => p.image) must be Id<"_storage"> values.

#

3. Why getAll “gives an error”

The knowledge sources don’t show your exact runtime/TS error, so I can’t say precisely which of these is failing, but based on the docs the likely causes are:

  • ctx.db.get(id) instead of ctx.db.get("image", id).[Reading data]
  • Type mismatch between p.image and v.id("image") / v.id("_storage").
  • Not handling null from db.get or storage.getUrl.[StorageReader.getUrl]

If you paste the exact error message, I can map it directly to one of these, but the “best” version of your pattern, grounded in the docs, is one of the two handlers above depending on whether images are table ids or storage ids.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.

woven raven
#

@earnest condor if i do not return imagesUrl i dont get any error and the type is (string | null)[]
but if i try to return it the return type become any

earnest condorBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This is TypeScript’s circular type inference issue, the same one Convex documents for actions and that also shows up with ctx.runQuery / ctx.runMutation / ctx.runAction and internal functions.[Circular inference; Discord circular; Discord tools]

Pattern:

  • If you use the result of ctx.runQuery(...) (or an internal query) in your return value, TS tries to infer the outer function’s return type from the inner one.
  • But the inner one’s type itself comes from Convex’s generated types, which depend on the outer function.
  • TS gives up and falls back to any.

Convex’s docs show two ways to break this loop; both work for your getAll query.

Option 1: Type the handler’s return

export const getAll = query({
  args: {},
  handler: async (ctx): Promise<[Doc<"pubblicita">[], (string | null)[]]> => {
    const pubblicita = await ctx.db
      .query("pubblicita")
      .withIndex("by_deleted", (q) => q.eq("deleted", false))
      .collect();

    const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
      images: pubblicita.map((p) => p.image),
    });

    return [pubblicita, imagesUrl];
  },
});

(Adjust Doc<"pubblicita"> import from ./_generated/dataModel as needed.)[TS best practices]

Option 2: Type the intermediate result

export const getAll = query({
  args: {},
  handler: async (ctx) => {
    const pubblicita = await ctx.db
      .query("pubblicita")
      .withIndex("by_deleted", (q) => q.eq("deleted", false))
      .collect();
#

const imagesUrl: (string | null)[] = await ctx.runQuery(
internal.image.query.getAllUrl,
{ images: pubblicita.map((p) => p.image) }
);

return [pubblicita, imagesUrl];

},
});


This is exactly the workaround Convex recommends for actions calling queries; the same reasoning applies here.[[Circular inference](<https://docs.convex.dev/functions/actions#dealing-with-circular-type-inference>); [Discord action calling action](<https://discord.com/channels/1019350475847499849/1414623864079515658>); [Discord tools](<https://discord.com/channels/1019350475847499849/1425094023550599260>)]

TypeScript will still check that `(string | null)[]` matches what `getAllUrl` actually returns, so you don’t lose type safety.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------