Backend API (/v1)
Every managed Postgres instance is a full backend: an instant REST API with row-level security, auth for your app's users, file storage, and edge functions — all under one base URL. Your site can be hosted anywhere (any static host, CDN, or mobile app); it talks to Pulsar with two keys.
Base URL: https://api.pulsardeploy.com/v1/<instance-id> /rest/<table> database over REST (RLS enforced) /auth/signup · /signin your app's end-users /auth/otp/send · verify passwordless email sign-in /functions/<slug> edge functions /storage/v1/object/… file storage /mail/send transactional email (service key) /realtime live change feeds (websocket)
Keys & the Public API switch
Open your database → API tab. You get an anon key (safe to ship in browser code — row-level security decides what it can see) and a service_role key (server-side only — bypasses RLS; never put it in a browser). Keys go in the apikey header or as a bearer token.
Everything public is dead until you flip Public APIon in that tab — anon reads, end-user signup/signin, and end-user storage all 403 while it's off. The same tab holds the CORS origin allowlist: lock it to your real domains before production.
Quick start with pulsar.js
<script type="module">
import { createClient } from 'https://pulsardeploy.com/pulsar.js';
const pulsar = createClient(
'https://api.pulsardeploy.com/v1/<instance-id>',
'<anon key>'
);
const { data, error } = await pulsar
.from('todos').select('id,title').eq('done', false).order('id').limit(20);
</script>Works in Node 18+ too — same import, no build step, no dependencies.
Database REST
GET /rest/todos?done=eq.false&order=id.desc&limit=20 list POST /rest/todos insert (object or array of objects) PATCH /rest/todos?id=eq.3 update (body = fields to set; filter required) DELETE /rest/todos?id=eq.3 delete (filter required) GET /rest/ schema introspection
Also: select=col1,col2, order=col.asc|desc, limit, offset. Values are always parameterized; identifiers are sanitized.
Row-level security
Three database roles map to your credentials: anon (anon key), authenticated (a signed-in end-user; auth.uid() returns their id), and service_role/owner (bypasses RLS). Tables created in the Table Editor start with RLS enabled — write policies in the Policies tab or SQL:
ALTER TABLE todos ENABLE ROW LEVEL SECURITY; -- anyone may read rows flagged public CREATE POLICY read_public ON todos FOR SELECT TO anon USING (is_public); -- signed-in users own their rows (user_id text DEFAULT auth.uid()) CREATE POLICY own_rows ON todos FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
Auth for your users
await pulsar.auth.signUp({ email, password }); // 201 → { userId, token }
await pulsar.auth.signIn({ email, password }); // session persists in localStorage
// every later call runs as this user — RLS applies automatically
await pulsar.from('todos').insert({ title: 'mine' });
pulsar.auth.signOut();Raw HTTP: POST /auth/signup and POST /auth/signin with { email, password } return a JWT — send it as Authorization: Bearer. Manage users, providers and signup settings in the database's Auth tab.
Passwordless sign-in (email OTP)
No password to store, nothing to forget: Pulsar emails a 6-digit code from its own mail server (no third-party email provider anywhere), you verify it, and the user is signed in with the exact same session + RLS behavior as password auth. Codes expire in 10 minutes, are single-use, hashed at rest, and rate-limited (60s resend cooldown, 5 verify attempts).
await pulsar.auth.signInWithOtp({ email }); // sends the code
const { data, error } =
await pulsar.auth.verifyOtp({ email, token: '123456' }); // signed in
// Raw HTTP: POST /auth/otp/send { email }
// POST /auth/otp/verify { email, code } → { userId, token }While testing (before the platform's outbound mail is fully unlocked), open Email → Send log in the dashboard — every OTP message and its code is visible there to you, the owner.
Social sign-in (OAuth)
Let users sign in with Google, GitHub, or GitLab. In the database's Auth tab: enable a provider, register an OAuth app with that provider, paste its Client ID & Secret, and copy the shown Callback URLinto the provider's app settings. Add your site's URL to the allowed redirect URLs so the login can return to it. Then:
pulsar.auth.signInWithOAuth({ provider: 'google' }); // or 'github', 'gitlab'
// → redirects to the provider, then back to your site signed in.
// The SDK captures the token from the return URL automatically;
// pulsar.auth.getSession() works from then on, same as password/OTP.Raw flow (no SDK): send the user to GET /auth/oauth/<provider>/start?final_redirect=<your-url>. After login they return to <your-url>#access_token=…&user_id=… — read the token from the URL fragment and send it as Authorization: Bearer.
Transactional email
Send product email — receipts, magic links, notifications — straight from your backend, through Pulsar's own MTA with DKIM signing. Server-side only: it requires the service_role key. Add your domain in Email (dashboard sidebar), publish the three DNS records it gives you, hit Verify — then send from any address on that domain. Until a domain is verified you can test from the shared platform domain to your own account email.
// Node / edge function / your server — NEVER the browser
const service = createClient(url, SERVICE_ROLE_KEY);
await service.mail.send({
from: 'Orders <[email protected]>',
to: '[email protected]',
subject: 'Your order #1042 shipped',
html: '<h1>On its way!</h1>',
reply_to: '[email protected]', // optional
});
// → 202 { id, status: 'queued' } (retries with backoff, log in Email page)Email templates
Design reusable emails with {{variable}} placeholders in Email → Templates(live preview + test send included), then send them by name — no HTML in your codebase. Variable values are HTML-escaped automatically; the template's subject can use variables too, and an explicit subject in the call overrides it.
await service.mail.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
template: 'welcome-email', // the template's name
variables: { name: 'Ada', action_url: 'https://acme.com/start' },
});Edge functions
const { data } = await pulsar.functions.invoke('hello', { body: { name: 'world' } });
// or: POST /functions/hello (any valid key or user JWT)Storage
Buckets live under your instance (create them in the Storage tab or with the service key). A public bucket serves files to anyone at a stable URL. End-users can read/write their own prefix — users/<their-id>/… — in any bucket, with zero policy setup. Everything else needs the service key or a signed URL.
// browser, signed-in user uploads their avatar
const uid = pulsar.auth.getUser().id;
await pulsar.storage.from('avatars').upload(`users/${uid}/pic.png`, file);
// public assets
const { data: { publicUrl } } = pulsar.storage.from('assets').getPublicUrl('logo.png');
// short-lived link to a private object (server-side)
const { data } = await service.storage.from('private').createSignedUrl('report.pdf', 3600);Realtime
Subscribe to database changes over a websocket — new rows, edits, and deletes arrive the moment they commit. Enable it per table in the database's Realtime tab (plus the Public API switch), then:
pulsar.channel('feed')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'todos', filter: 'done=eq.false' },
(payload) => {
// payload.eventType 'INSERT' | 'UPDATE' | 'DELETE'
// payload.new the row (INSERT/UPDATE)
// payload.old primary key of the previous row
render(payload);
})
.subscribe((status) => console.log(status)); // 'SUBSCRIBED'
// later: pulsar.removeChannel(ch) or pulsar.removeAllChannels()Realtime respects your row-level security: an anon or signed-in subscriber only receives an INSERT/UPDATE if their identity could SELECTthat row under your policies — checked per event, per subscriber. DELETE events carry only the primary key (the row is gone, so policies can't be evaluated), and tables without a primary key emit nothing to public subscribers. The service_role key sees every change in full, on any table, with no opt-in. Sign-ins upgrade a live socket in place — after auth.signIn()the same channel starts receiving that user's private rows.
Raw websocket (no SDK): connect to wss://api.pulsardeploy.com/v1/<instance-id>/realtime?apikey=… and send {action:'subscribe', table:'todos'} — messages are plain JSON both ways.
AI Gateway
A managed, OpenAI-compatible proxy to any LLM provider — with response caching, per-token cost tracking, and a monthly budget cap. Enable it in the database's AI Gateway tab, add your provider key(s), and point any OpenAI client at the gateway base URL (service_role key only — never the browser):
const openai = new OpenAI({
baseURL: "https://api.pulsardeploy.com/v1/<instance-id>/ai/gateway",
apiKey: SERVICE_ROLE_KEY,
});
await openai.chat.completions.create({ model: "gpt-4.1-nano", messages });Routing follows the model name (e.g. gpt-* → OpenAI, llama-*→ Groq) or an explicit provider/modelprefix. Identical requests are served from cache (no provider cost), every call's tokens and cost are metered in the tab, and requests are refused once the monthly budget is hit.
Security checklist
1. Never ship the service_role key to a browser. 2. Keep RLS on and write policies before enabling Public API. 3. Lock CORS to your real origins. 4. Rotate keys from the API tab if anything leaks — every existing key dies instantly. Public endpoints are rate-limited per IP.