n8n-io/n8n · error

Supabase upsert failed: ${error.message}

Error message

Supabase upsert failed: ${error.message}

What it means

Thrown by SupabaseVectorStore.upsert when the PostgREST upsert call returns a non-null `error` object. The underlying message from the Supabase/PostgREST client is appended, so the root cause is typically RLS denial, a missing table, a schema/column mismatch, a unique-constraint violation not handled by onConflict, or a network/auth problem. This is a runtime/backend error, not input validation.

Source

Thrown at packages/@n8n/agents/src/vector-stores/supabase.ts:122

		super(name, options);
		this.tableName = options.tableName;
		this.queryName = options.queryName ?? 'match_documents';
	}

	async upsert(records: VectorRecord[]): Promise<void> {
		if (records.length === 0) return;

		const client = await this.getClient();
		const { error } = await client.from(this.tableName).upsert(
			records.map((record) => ({
				id: record.id,
				content: record.content,
				metadata: record.metadata,
				embedding: record.vector,
			})),
			{ onConflict: 'id' },
		);
		if (error) throw new Error(`Supabase upsert failed: ${error.message}`);
	}

	async query(
		vector: number[],
		opts: { topK: number; filter?: VectorFilter },
	): Promise<VectorQueryResult[]> {
		const client = await this.getClient();
		const rpcCall = client.rpc<string, MatchDocumentsFn>(this.queryName, {
			query_embedding: vector,
		});
		const filtered =
			opts.filter && opts.filter.conditions.length > 0
				? applySupabaseFilter(rpcCall, opts.filter)
				: rpcCall;

		const { data, error } = await filtered.limit(opts.topK);
		if (error) throw new Error(`Supabase query failed: ${error.message}`);

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Read the appended PostgREST message first — it names the concrete failure (e.g. 'relation does not exist', 'permission denied', 'violates unique constraint').
  2. For RLS, use the service role key for trusted backend writes, or add an INSERT/UPDATE policy for the table.
  3. Verify the table exists with columns id, content, metadata (jsonb), embedding (vector(n)) matching the embedded dimension.
  4. For transient/network errors, retry with backoff.

Example fix

// before — anon key blocked by RLS
new SupabaseVectorStore('docs', {
  url, apiKey: process.env.SUPABASE_ANON_KEY, tableName: 'docs',
});

// after — service role key for trusted backend writes
new SupabaseVectorStore('docs', {
  url, apiKey: process.env.SUPABASE_SERVICE_ROLE_KEY, tableName: 'docs',
});
Defensive patterns

Strategy: retry

Validate before calling

function assertSupabaseWritable(opts: { url: string; apiKey: string; tableName: string }): void {
  if (!/^https?:\/\/.+/.test(opts.url)) throw new Error('Supabase url missing/invalid');
  if (!opts.apiKey) throw new Error('Supabase apiKey missing (use service role key for writes)');
  if (!opts.tableName) throw new Error('Supabase tableName missing');
}

assertSupabaseWritable(opts);
new SupabaseVectorStore('docs', opts);

Type guard

function looksLikeServiceRoleKey(key: string): boolean {
  // Supabase service role keys are long JWTs; anon keys are too, so this is a heuristic —
  // the real test is whether writes succeed (RLS rejects anon-role writes).
  return /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(key);
}

Try / catch

async function upsertWithRetry(store: SupabaseVectorStore, records: VectorRecord[], attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      await store.upsert(records);
      return;
    } catch (err) {
      const msg = err instanceof Error ? err.message : '';
      const transient = /network|timeout|fetch|ECONN|503|504|paused/i.test(msg);
      if (!transient || i === attempts - 1) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 200));
    }
  }
}

Prevention

When it happens

Trigger: RLS policy blocking inserts/updates for the api key's role; tableName pointing at a non-existent table; embedding column missing or wrong type; metadata column not jsonb; onConflict:'id' failing because `id` is not the primary key; service role key expired/wrong; Supabase project paused/unreachable.

Common situations: Using the anon key instead of the service role key for writes; RLS enabled without a permissive policy for the backend role; migrating the table schema and forgetting the embedding column; Supabase free-tier project auto-pausing; network egress blocked.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/e08e7c1fb6e4d9e3. Report an issue: GitHub.