Skip to content

Troubleshooting

Requests with no signed-in user read nothing unless the table has a public rule for reading. For user data, send the user’s access token and give the table a claim row rule.

  1. List the table’s rules with GET /rls/policies/{projectId}, using a console access token.
  2. A claim rule returns only rows whose column equals a claim in the caller’s access token, such as author_id equal to sub. Check that the rows hold the signed-in user’s id in that column, and that the request carries the user’s token (accessToken in createVaifClient, or an Authorization: Bearer header). An expired token, or one from another project, counts as no token.
  3. Compare with the console’s SQL editor, which row rules do not limit.
  1. The key starts with vk_pub_, vk_srv_ or vaif_, and is still listed on Security > Credentials.
  2. The project ID is the one the key belongs to. A key for another project is refused.
  3. The key was created in the environment you are calling. Keys belong to one environment.
  4. baseUrl is https://api.vaif.studio.
  5. Read the code in the response, such as TOKEN_EXPIRED or INVALID_API_KEY. See Error codes.
import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_PROJECT_ID,
apiKey: process.env.VAIF_PUBLISHABLE_KEY,
});

Management calls return 403 SCOPE_INSUFFICIENT

Section titled “Management calls return 403 SCOPE_INSUFFICIENT”

Schema changes, row rules, billing and project administration need a console access token, not an API key. See Access tokens.

  1. Realtime must be on for the table: turn it on under Realtime > Tables, or on the table’s Realtime tab in the Table Editor.
  2. The client needs an API key, and the user’s access token when row rules apply.
  3. Watch the subscription status: CHANNEL_ERROR means the server refused it, for example for an unknown table or column, or a filter realtime cannot apply.

See Realtime.

Wait for the number of seconds in Retry-After, and avoid polling: subscribe to changes with realtime instead.

import { VaifRateLimitError } from "@vaifstudio/client";
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (error) {
if (!(error instanceof VaifRateLimitError) || i >= attempts - 1) throw error;
const seconds = error.retryAfter ?? 2 ** i;
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
}
const { data: posts } = await withRetry(() => vaif.from("posts").limit(20).list());
console.log(posts.length);

The SDK already retries reads (GET) on network errors and on 500, 502, 503 and 504. A read that gets a 429 is retried once, never sooner than its Retry-After. The SDK never replays a write on its own.

Add indexes on the columns you filter and sort by, fetch only the columns you need, and page through results. Add an index on the Schema page, or in a migration file applied with vaif db push:

CREATE INDEX idx_posts_created ON posts (created_at);
const page = 2;
const { data } = await vaif
.from("posts")
.select("id", "title", "created_at")
.orderBy("created_at", "desc")
.limit(20)
.offset(page * 20)
.list();
console.log(data);

A list returns 50 rows unless you set limit, and at most 1000 rows per request; a higher limit is refused with LIMIT_TOO_HIGH.

Check that the bucket exists, and that its policies let the current user upload to that path, with that content type and size. Uploads also count toward your plan’s storage limit; see Plans.

Email support@vaif.studio with the requestId from the error response.