
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
@staticbot/base44-supabase-shim
Advanced tools
Drop-in shim mimicking @base44/sdk API but routing to a Supabase backend. Vendored into migrated apps by Staticbot's Base44 native migration so the customer's build doesn't need a live npm connection.
Drop-in shim that exposes the same surface as @base44/sdk but routes
every call to a Supabase backend (Postgres + GoTrue + Storage + Edge
Functions). Built so you swap one import in src/api/base44Client.js and
your hundreds of pages keep working.
This is the runtime shim Staticbot's Base44 → Supabase migration vendors into your repo as the final step of the migration. You can also use it standalone if you're doing the migration by hand.
Staticbot's Base44 native migration vendors the prebuilt dist/ directly into
your repo at vendor/base44-supabase-shim/ and patches package.json to point
at it. You don't need to do anything manually — just merge the PR that lands
on the staticbot/base44-switchover-<id> branch.
In each consuming app, vendor the prebuilt tarball or add as a git submodule (so builds don't depend on an npm registry):
# Option A: git submodule (you'll need to build it once locally)
git submodule add https://github.com/staticbot/staticbot-base44-supabase-shim.git vendor/base44-supabase-shim
cd vendor/base44-supabase-shim && npm i && npm run build
# Option B: vendor the prebuilt tarball straight from the npm registry
# (same URL shape staticbot-app's Base44ShimProperties.getDistTarballUrl builds)
curl -L https://registry.npmjs.org/@staticbot/base44-supabase-shim/-/base44-supabase-shim-0.6.1.tgz \
| tar -xz -C vendor/base44-supabase-shim --strip-components=1
In package.json:
{
"dependencies": {
"@staticbot/base44-supabase-shim": "file:./vendor/base44-supabase-shim",
"@supabase/supabase-js": "^2.45.0"
}
}
// src/api/base44Client.js — replace @base44/sdk import
import { createClient } from '@staticbot/base44-supabase-shim';
export const base44 = createClient({
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
schemaPrefix: 'propertyflow', // app-specific schema
sharedSchema: 'core', // shared entities live here
// sharedEntities default: ['Customer','Company','User','Role','Department','Notification','AuditLog']
});
// Then everything in your existing pages keeps working:
const customers = await base44.entities.Customer.list('-created_date', 50);
const c = await base44.entities.Customer.get('uuid');
await base44.entities.Customer.update('uuid', { phone: '081...' });
await base44.entities.Unit.create({ unit_no: 'A-101' });
import { createClientFromRequest } from 'npm:@staticbot/base44-supabase-shim/server';
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req, {
supabaseUrl: Deno.env.get('SUPABASE_URL')!,
supabaseAnonKey: Deno.env.get('SUPABASE_ANON_KEY')!,
supabaseServiceRoleKey: Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
schemaPrefix: 'propertyflow',
});
const customers = await base44.asServiceRole.entities.Customer.filter(
{},
'-created_date',
50,
);
return new Response(JSON.stringify({ data: customers }));
});
Default rule: PascalCase → snake_case + plural.
Customer → customersChartOfAccount → chart_of_accountsMeetingMinute → meeting_minutesOverride via entityMap if a real table doesn't follow the rule:
createClient({
...,
entityMap: {
Customer: { schema: 'core', table: 'customers' }, // explicit
Job: { schema: 'construction', table: 'project_jobs' }, // non-default name
},
});
// Equality (default)
await base44.entities.Customer.filter({ status: 'active', vip: true });
// IN (pass array)
await base44.entities.Customer.filter({ id: ['a', 'b', 'c'] });
// Operators (Mongo-style, as Base44 apps write them)
await base44.entities.Invoice.filter({
amount: { $gt: 1000 },
customer_name: { $ilike: '%co.%' },
});
// Case-insensitive regex search (common in migrated server functions)
await base44.entities.Provider.filter({ name: { $regex: '^acme', $options: 'i' } });
// Presence check
await base44.entities.Profile.filter({ avatar_url: { $exists: true } });
// Boolean OR across sub-filters
await base44.entities.Argument.filter({
$or: [{ created_by_id: userId }, { author_id: userId }],
});
Field operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like,
$ilike, $regex (+ $options: 'i'), $exists. Top-level combinators: $or, $and
(over an array of sub-filters; multiple fields within one sub-filter are AND-ed). The legacy
{ op, value } form is still accepted. $nor/$not and regex/like inside a $or group
throw a clear error rather than silently returning wrong rows.
'name' ascending, '-created_date' descending.{ field: 'name', ascending: false }.list(options) / filter(query, options) with an options object return one page,
{ items, next_cursor, has_more }, instead of an array. Options: sort (default
'-created_date'), limit (default 100, max 5,000), cursor, fields (id is always
included). { distinct: 'field' } returns the field's distinct values, ascending, with array
fields contributing each element. The positional forms still return arrays. The page is
itself the array of items, with items / next_cursor / has_more attached as
non-enumerable properties: apps written against older SDKs passed the same options object
(which those SDKs ignored) and use the result as an array, so both styles keep working.count(query?) returns the number of matching records.aggregate({ query, groupBy, dateBucket, count, sum, avg, min, max, countDistinct, having, sort, limit })
returns { rows, truncated } with Base44's row keys (count, sum_<f>, avg_<f>, ...).
Supabase's PostgREST ships with aggregates disabled, so the shim reads the matching rows
(only the columns the spec names) and groups them in the client. It stops reading at 50,000
rows and then reports truncated: true. dateBucket groups by the bucket's start date
(YYYY-MM-DD, UTC, weeks start Monday), stored under the date field's name.updateMany(query, update) takes Base44's MongoDB update document: $set, $unset,
$currentDate, $inc, $mul, $min, $max, $addToSet (with $each), $push (with $each,
$position, $sort, $slice), $pull (value, { $in | $gt | ... } predicate, or partial
document) and $rename. A flat patch still works and means $set. One field may appear in one
operator only; an unknown operator, or operators mixed with plain fields, throws.$set / $unset / $currentDate alone are a single PATCH over every matching row. Every other
operator needs the current value, so the shim reads up to 500 matching rows and writes each back
with a compare-and-set: the PATCH repeats the query and requires the columns it read to be
unchanged; a row a concurrent writer changed first is re-read (jittered backoff) and retried. So
updateMany({ id, credits: { $gte: 5 } }, { $inc: { credits: -5 } }) never takes a balance below
zero and concurrent $incs never lose one. Like Base44, at most 500 rows per call — call again
while has_more.success, updated and
has_more as non-enumerable properties.update(id, data) accepts the same operators for one record.upsert(records, { key }) (single field or array) updates the records whose key values match an
existing row (merging the given fields) and creates the rest; the last record per key in one call
wins. Returns { created, updated, records }. Base44 enforces key uniqueness itself; a migrated
table usually has no unique index on the key, so two concurrent upserts of a brand-new key can
both insert. With a unique index the loser's 23505 is retried once and becomes an update.deleteMany(query) returns { success, deleted }; delete(id) returns { success }.Core.UploadFile / Core.UploadPublicFile return Base44's { file_url } (plus url and path).
Before 0.8.3 they returned only { url, path }, and every app reading file_url stored undefined.Core.UploadPrivateFile({ file }) uploads to integrations.privateBucket (default
<defaultBucket>_private, which must exist and must not be public) and returns
{ file_uri: 'supabase-storage://<bucket>/<path>' }. Core.CreateFileSignedUrl({ file_uri, expires_in })
signs it (60–3600 s, default 300). A file_uri from Base44 itself is not in Supabase Storage and
is rejected with that reason.Core.<Endpoint> rejects with "not available after migrating off Base44" instead of a
TypeError.auth.me()Base44's User entity maps to the shared core.users table (Staticbot's migration exposes the core
schema and gives it Base44's User rules: a user sees and edits their own row, an admin all rows, and
only an admin can change a role). auth.me() / getUser() build the user from, in rising precedence:
user_metadata → app_metadata → the signed-in user's own row. The row is authoritative for every
column it has, NULL included, so me().role is the same role the database's RLS checks — including
after an admin changes it. user_metadata is writable by the user (signUp({options: {data}})), so
role and the other protected keys are never taken from it; app_metadata (service-role only) is the
trusted fallback when the row isn't reachable. auth.updateMe() updates user_metadata and mirrors
the keys that are columns of the row (never id, email, role or system columns). Pass
authOptions: { usersTable: null } to opt out of the row overlay.
Base44 fills id, created_by_id and created_by on its servers and returns the created record
whatever the entity's read rules say. create() / bulkCreate() keep that contract:
created_by_id /
created_by taken from the signed-in session, and a not-null error on id with a Base44-shaped
24-hex id.A failed insert writes nothing, so retries never duplicate. A refusal that survives every step throws the original error. Without a session (service role, anonymous) nothing is stamped.
Base44 alias names are supported alongside Supabase-idiomatic ones so migrated apps compile unchanged:
auth.signIn({email, password}) / auth.loginViaEmailPassword(email, password)auth.signUp({email, password, metadata?}) / auth.register({email, password, metadata?})auth.loginWithProvider(provider, returnPath?) — Google / Microsoft / Facebook / Apple; builds an absolute redirectTo from window.location.origin.auth.verifyOtp({email, otpCode}) — email OTP after register(). Returns {access_token, session, user}; the Supabase session is installed as a side-effect so a follow-up setToken() is a no-op.auth.resendOtp(email)auth.setToken(accessToken) — kept as a warn-once no-op so migrated code doesn't TypeError; the session is already installed by verifyOtp / signInWithPassword.auth.resetPasswordRequest(email, {redirectTo?}) — Supabase resetPasswordForEmail.auth.resetPassword({resetToken, newPassword}) — resetToken is ignored (Supabase already exchanged the link fragment for a session before you got here).auth.isAuthenticated(): Promise<boolean>auth.logout(returnUrl?) — after signOut resolves, navigates to returnUrl if provided.auth.me() / auth.updateMe(metadata) / auth.getUser() / auth.getSession() / auth.redirectToLogin(returnUrl?) / auth.onAuthStateChange(cb)app.getPublicSettings() — resolves { id: null, public_settings: {} }. Newer Base44 scaffolds await it in AuthContext before checking the session; there are no Base44 app-level settings on self-host, so the stub just lets that gate pass.base44.agents.* (Superagents) runs on the Staticbot AI agent runtime in your own Supabase
project: conversations and messages live in the staticbot_ai schema (each user sees only their
own), and the staticbot-agents edge function runs the agent's tool loop against any
OpenAI-compatible AI provider. Tools run with the signed-in user's permissions, so row-level
security applies to everything an agent reads or writes.
Supported, in the SDK's shape so existing UI code works unchanged:
listConversations({ agent_name }), getConversations(), getConversation(id),
createConversation({ agent_name, metadata }), addMessage(conversation, { role, content }),
subscribeToConversation(id, onUpdate) (Supabase Realtime; replies stream in as the agent works).
addMessage resolves once the message is stored; the reply arrives through
subscribeToConversation (or the next getConversation).
WhatsApp / Telegram bridges aren't part of the runtime. Their connect URLs return null unless
configured, so <a href={...}> renders as an inert link:
createClient({
...,
agents: {
functionName: 'staticbot-agents', // default
schema: 'staticbot_ai', // default
whatsappUrls: { SubdomainReviewer: 'https://wa.example.com/connect' },
telegramUrls: { Hilton: 'https://t.me/hilton_bot' },
},
});
integrations.sendEmailFunction.InvokeLLM / GenerateImage — opt-in only. Wire your own endpoint
through an edge function and pass its name via integrations.invokeLlmFunction
/ integrations.generateImageFunction.SendSMS / ExtractDataFromUploadedFile — opt-in edge functions
(integrations.sendSmsFunction / integrations.extractDataFunction). Both are
implemented as methods that throw a clear "not configured" error when unset, so a
migrated app that calls them fails legibly instead of hitting an undefined TypeError.importEntities (CSV/file import) — not implemented.subscribe event shape — callbacks get { type: 'insert' | 'update' | 'delete', new, old }
(Supabase Realtime), not the SDK's { type: 'create' | ..., data, id }.npm i
npm test # vitest, mocked SupabaseClient + real postgrest-js over a fake fetch
npm run build # tsup → dist/
tests/entities.e2e.test.ts runs create() against a real Postgres + PostgREST (RLS ordering,
RETURNING vs the SELECT policy and missing=default are server behaviour no mock can prove). It is
skipped unless pointed at one:
docker network create shimnet
docker run -d --name shimpg --network shimnet -e POSTGRES_PASSWORD=pw \
-v "$PWD/tests/e2e/init.sql:/docker-entrypoint-initdb.d/init.sql" postgres:16
docker run -d --name shimrest --network shimnet -p 3999:3000 \
-e PGRST_DB_URI=postgres://authenticator:pw@shimpg:5432/postgres -e PGRST_DB_SCHEMAS=public \
-e PGRST_DB_ANON_ROLE=anon -e PGRST_JWT_SECRET=a-string-secret-at-least-32-bytes-long-xx \
postgrest/postgrest:v12.2.3
SHIM_E2E_PGRST_URL=http://localhost:3999 SHIM_E2E_JWT_SECRET=a-string-secret-at-least-32-bytes-long-xx \
npx vitest run tests/entities.e2e.test.ts
FAQs
Drop-in shim mimicking @base44/sdk API but routing to a Supabase backend. Vendored into migrated apps by Staticbot's Base44 native migration so the customer's build doesn't need a live npm connection.
The npm package @staticbot/base44-supabase-shim receives a total of 195 weekly downloads. As such, @staticbot/base44-supabase-shim popularity was classified as not popular.
We found that @staticbot/base44-supabase-shim demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.