# vibebackr
> Give any tool you build in Claude or ChatGPT its own live web link, Google sign-in, and cloud memory that stays saved - zero coding or servers required.
- Website: https://vibebac.kr
- GitHub: https://github.com/eliotstocker/vibebackr
- MCP Endpoint: https://vibebac.kr/mcp
# Turn What You Build in AI Chat Into a Real App Anyone Can Use
When you ask Claude, Gemini, or ChatGPT to build you a tool, the preview is trapped inside your chat window - refresh the page and everything you typed disappears, and nobody else can open it. **VibeBackr turns your AI creations into real websites with their own live link, Google sign-in, and cloud memory that stays saved.**
- [Get Started in 60 Seconds](#how-it-works)
- [Sign In & Open Console](/login)
## How It Works: Pick Your AI & Start in 60 Seconds
You don't need to write code, buy a domain, or set up a server. Choose the AI assistant you use and follow the 3 steps:
### Claude
### Add VibeBackr to Claude
On **Claude Desktop**, download the 1-click extension and open it. Or in **Claude Settings > Connectors**, add a custom connector with our URL and sign in once with Google:
- [Download for Claude (1-Click)](/vibebackr.mcpb)
- `https://vibebac.kr/mcp`
### Ask Claude to Build Your App
Describe the tool you want in plain English - *"Build a shared family recipe book"* or *"Make a client request tracker my team can sign into."*
### Open Your Live Link & Share It
Claude builds your app and publishes it straight to its own web address (like `your-app.vibebac.kr`). Open it on your phone or send it to anyone.
### Gemini
### Paste the URL in Customizations
Open the **Customise > Connectors > + Custom** popup, choose to connect an MCP server, paste this URL, click next, follow onscreen prompts to sign in once with your Google account:
- `https://vibebac.kr/mcp`
### Ask Gemini to Build Your App
Tell Gemini what you want to make in plain English - *"Build a group trip planner where friends can sign in with Google and vote on hotels."*
### Open Your Live Link & Share It
Gemini sets up your cloud memory and publishes your finished app straight to its own live web link (`your-app.vibebac.kr`).
### ChatGPT
### Connect in ChatGPT Settings
Open **Settings > Connectors > Create Custom Connector** in ChatGPT, paste this URL, and sign in once with your Google account:
- `https://vibebac.kr/mcp`
### Ask ChatGPT to Build Your App
Enable the VibeBackr connector in your chat and describe the tool you want in plain English - zero coding required.
### Open Your Live Link & Share It
ChatGPT creates your cloud storage and publishes your app to its own live web address (`your-app.vibebac.kr`) ready to share with anyone.
### Cursor & Copilot
### Add the MCP Server URL
In Cursor, Windsurf, or VS Code (GitHub Copilot), open **MCP Settings > Add Server**, paste this URL, and authorize once in your browser:
- `https://vibebac.kr/mcp`
### Ask Your Agent to Build & Deploy
Ask your agent to build a tool with Google login and saved cloud data. It automatically provisions the database and wires up sign-in.
### Instant Live Hosting
Your agent deploys your app straight to `your-app.vibebac.kr` - or connects your local React/Next.js frontend in one line.
### Other AI (Copy Prompt)
Using a regular web chat without custom connectors? Just copy and paste this starter prompt into **any** AI chat alongside your idea:
Use VibeBackr (https://vibebac.kr/llms.txt) to build, store, and host my app so it saves data in the cloud and has Google sign-in.
- If you support MCP: connect to https://vibebac.kr/mcp, create a collection (turn on auto_invite_shares if people can share items), and publish the finished app with publish_app so I get a live shareable link.
- If you don't support MCP: give me the collection setup to paste into https://vibebac.kr/login, then build the single-page app using with Google sign-in (vibe.loginWithGoogle()).
## What VibeBackr Gives Your AI Apps
Normally, AI previews are just temporary drawings on your screen. VibeBackr gives your AI the missing pieces so your app actually works in the real world:
### A Real Web Link You Can Share
Every app gets its own live web address (`your-app.vibebac.kr`) that opens on any phone, tablet, or laptop - ready to bookmark or drop into a group chat.
### Cloud Memory That Never Disappears
Whatever you or your guests type, add, or check off is saved safely in the cloud. Close the tab, come back next week, or switch devices and everything is still there.
### Google Sign-In & Automatic Invites
Keep your app **private** just for you, **share** specific items with someone's email address (they'll be invited automatically!), or make it **public** for anyone with the link.
## What People Build With VibeBackr
If you can describe it in a sentence, your AI can turn it into a working tool you can use today.
### Work & Team Micro-Apps
Client portals, lightweight CRMs, project and OKR trackers, and team request forms that your whole team can sign into with Google.
### Personal & Family Tools
Shared family recipe books, group trip planners, household trackers, and workout logs you can keep on your phone's home screen.
### Community & Event Tools
Club signup sheets, event RSVP pages, tournament brackets, and feedback boards you can share with a single link.
### Founder Prototypes & MVPs
Test a new product idea with real users, real Google sign-ins, and saved customer data in an afternoon - before hiring developers.
## Simple, Transparent Pricing
Start with a 7-day free trial on our managed cloud, or self-host the open-source version for free forever.
> **Built for speed and reliability:** Every app you create gets its own dedicated cloud storage and traffic allowance, so one busy tool never slows down your others. Curious how the tech works under the hood? Check out [For Nerds](/nerds).
---
# Page: Frontend Client Integration Guide
# Frontend Client Integration Guide
Turn any React, Next.js, Vue, Svelte, or single-file HTML prototype into a live, multi-user web app with login, permissions, auto-invites, and cloud storage - zero backend code required.
- `https://vibebac.kr/vibebackr.js`
- [Open Console & Sign In](/login)
## Overview & Data Model
Every frontend connects to the same canonical endpoint and queries isolated tenant collections.
### Canonical Endpoint & Instant Hosting
Agents and frontends always target `https://vibebac.kr`, and single-file HTML apps can be published directly to `https://{dashless_collection_id}.vibebac.kr` via `publish_app`.
### Universal Records
Every record includes a `title`, markdown `content`, and a flexible JSON `data` object for arbitrary structured payloads.
### 5-Tier Access Control & Auto-Invites
Permissions evaluate automatically: Owner, Manager Hierarchy, Public Domain, Email Shares, and Admin. Enable `auto_invite_shares` on a collection to automatically invite non-users when a record is shared with their email.
## Authentication for Frontends
Every authenticated request uses an `Authorization: Bearer ` header. VibeBackr acts as a centralized OAuth broker so you never need to register Google Cloud OAuth credentials or configure `localhost` redirect URLs.
### Popup Login (`file://` & SPAs)
Opens a centered popup to sign in with Google, transmits session tokens back via `window.postMessage`, and works even from local `file:///.../index.html` pages with no dev server.
### Full-Page Redirect
Redirects the browser to `/auth/login/google?redirect_uri=...` and returns to your app with `#access_token=...` and `#refresh_token=...` in the URL hash.
### Local `file://` Null Origins
Browsers assign `Origin: null` to local `file://` documents. VibeBackr permits `null` origins with Bearer tokens out of the box via CORS.
### CSP & Sandboxed Iframe Relay
When strict CSP (`connect-src 'self'`) or sandboxed iframes block direct `fetch`, the SDK automatically falls back to a hidden `/relay` iframe bridge over `postMessage`.
### Popup & Redirect Examples
```typescript
//popup sign-in works from file:// or http://localhost
client.login('google').then(auth => {
console.log('Logged in as', auth.user.email);
});
//full-page redirect
window.location.href = client.getLoginUrl('google', 'http://localhost:3000');
client.handleAuthCallback();
```
## Core REST Endpoints
All endpoints accept and return JSON over `https://vibebac.kr`.
| Endpoint | Description |
| :--- | :--- |
| **`GET /api/tenants`** | List all tenants the user owns or belongs to. |
| **`DELETE /api/tenants/{tenant_id}`** | Leave a joined tenant. |
| **`GET /api/collections`** | List all collections accessible to the authenticated user. |
| **`POST /api/collections`** | Create a collection (`name`, `default_visibility`, `origin_urls`, `vector_search_enabled`, `auto_invite_shares`, and optional seed `records`). |
| **`GET /api/collections/{id}`** | Fetch a single collection's metadata by ID. |
| **`PUT /api/collections/{id}`** | Update a collection's `name`, `default_visibility`, `origin_urls`, `vector_search_enabled`, or `auto_invite_shares`. |
| **`PUT /api/collections/{id}/app`** | Publish a single-file HTML frontend (`{ "html": "..." }`) to `https://{dashless_id}.vibebac.kr`. |
| **`DELETE /api/collections/{id}`** | Permanently delete a collection. |
| **`GET /api/collections/{id}/records`** | List records in a collection visible to the current user. |
| **`POST /api/collections/{id}/records`** | Create a new record with `title`, `content`, `data`, and `access_policy`. |
| **`GET /api/collections/{id}/records/{rec_id}`** | Fetch a single record by ID. |
| **`PUT /api/collections/{id}/records/{rec_id}`** | Update a record's `title`, `content`, `data`, or `access_policy`. |
| **`DELETE /api/collections/{id}/records/{rec_id}`** | Permanently delete a record. |
| **`POST /api/collections/{id}/search`** | 768-d vector semantic search (EmbeddingGemma 2) with automatic text fallback. |
### Creating & Searching Records
```typescript
fetch('https://vibebac.kr/api/collections/col_123/records', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Weekly Standup Notes',
content: 'Discussed sprint roadmap and migration.',
data: {priority: 'high', tags: ['eng', 'q4']},
access_policy: {visibility: 'shared', shares: ['colleague@example.com']}
})
});
fetch('https://vibebac.kr/api/collections/col_123/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({query: 'sprint goals and migration', limit: 10})
})
.then(res => res.json())
.then(results => {
console.log(results);
});
```
## Frontend SDK & Drop-In Client
Access tokens expire after 15 minutes. Both the hosted CDN SDK (`vibebackr.js`) and the drop-in TypeScript client automatically rotate refresh tokens via `POST /oauth/token` whenever a request receives `401 Unauthorized`.
### 1. CDN Script Tag (Zero Build Step)
Include [https://vibebac.kr/vibebackr.js](/vibebackr.js) (short alias: [/vibe.js](/vibe.js), TypeScript types at [/vibebackr.d.ts](/vibebackr.d.ts), full specification at [/llms-full.txt](/llms-full.txt)).
> **Note for AI Coding Assistants:** Never build token or API-key settings modals in generated apps. End users authenticate via `vibe.loginWithGoogle()`, or read/write public records directly when `default_visibility` is `"public"`.
```html
```
Or use the multi-collection factory via `VibeBackr.createClient()` / `createVibeClient()`:
```html
```
### 2. Drop-In TypeScript Client (React, Next.js, Vue, Vite)
Copy this self-contained client into `src/lib/vibeBackr.ts`:
```typescript
export type AccessPolicy = {
visibility: 'private' | 'public' | 'shared';
shares?: string[];
};
export type VibeRecord> = {
id: string;
collection_id: string;
title: string;
content: string;
data: string; //json string payload
created_by: string;
created_at: string;
updated_at: string;
access_policy: AccessPolicy;
};
export type SearchResult> = {
record: VibeRecord;
score: number;
method?: string;
};
export type VibeClientOptions = {
baseUrl?: string;
collectionId?: string;
tokenKey?: string;
refreshTokenKey?: string;
useRelay?: boolean;
relayUrl?: string;
};
export function createVibeClient(baseUrlOrOptions: string | VibeClientOptions = 'https://vibebac.kr', options: VibeClientOptions = {}) {
let baseUrl = 'https://vibebac.kr';
if(typeof baseUrlOrOptions === 'object' && baseUrlOrOptions !== null) {
options = baseUrlOrOptions;
baseUrl = options.baseUrl || 'https://vibebac.kr';
} else {
baseUrl = baseUrlOrOptions || options.baseUrl || 'https://vibebac.kr';
}
baseUrl = baseUrl.replace(/\/+$/, '');
const defaultCollectionId = options.collectionId || null;
const tokenKey = options.tokenKey || 'vb_token';
const refreshTokenKey = options.refreshTokenKey || 'vb_refresh_token';
const state: {refreshPromise?: Promise; relayPromise?: Promise} = {};
const getToken = () => (typeof window !== 'undefined' ? localStorage.getItem(tokenKey) : null);
const setRefreshToken = (t: string | null) => {
if(typeof window === 'undefined') {
return;
}
if(t) {
localStorage.setItem(refreshTokenKey, t);
} else {
localStorage.removeItem(refreshTokenKey);
}
};
const setToken = (t: string | null, rt?: string | null) => {
if(typeof window !== 'undefined') {
if(t) {
localStorage.setItem(tokenKey, t);
} else {
localStorage.removeItem(tokenKey);
}
}
if(rt !== undefined) {
setRefreshToken(rt);
}
};
const getRefreshToken = () => (typeof window !== 'undefined' ? localStorage.getItem(refreshTokenKey) : null);
function resolveColId(colId?: string): string {
const id = colId || defaultCollectionId;
if(!id) {
throw new Error('collectionId is required');
}
return id;
}
function refreshToken(): Promise {
if(state.refreshPromise) {
return state.refreshPromise;
}
const rt = getRefreshToken();
if(!rt) {
return Promise.reject(new Error('No refresh token available'));
}
const body = `grant_type=refresh_token&refresh_token=${encodeURIComponent(rt)}`;
const refreshOpts: RequestInit = {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body
};
(refreshOpts as any)._skipRefresh = true;
const refreshCall = options.useRelay
? requestViaRelay('/oauth/token', refreshOpts)
: fetch(`${baseUrl}/oauth/token`, refreshOpts).then(res => {
if(!res.ok) {
return res.json()
.catch(() => ({error: res.statusText}))
.then(err => {
throw new Error(err.error || `Token refresh failed: ${res.status}`);
});
}
return res.json();
});
state.refreshPromise = refreshCall
.then((data: any) => {
delete state.refreshPromise;
if(data?.access_token) {
setToken(data.access_token);
}
if(data?.refresh_token) {
setRefreshToken(data.refresh_token);
}
return data;
})
.catch(err => {
delete state.refreshPromise;
setToken(null);
setRefreshToken(null);
throw err;
});
return state.refreshPromise;
}
function getRelayWindow(): Promise {
if(typeof window === 'undefined') {
return Promise.reject(new Error('Relay transport requires browser window'));
}
if(state.relayPromise) {
return state.relayPromise;
}
state.relayPromise = new Promise(resolve => {
let frame = document.getElementById('vb-relay-frame') as HTMLIFrameElement;
if(!frame) {
frame = document.createElement('iframe');
frame.id = 'vb-relay-frame';
frame.src = options.relayUrl || `${baseUrl}/relay`;
frame.style.display = 'none';
frame.setAttribute('aria-hidden', 'true');
document.body.appendChild(frame);
}
const onMsg = (e: MessageEvent) => {
if(
e.data
&& (e.data.type === 'vb_relay_ready' || e.data.type === 'vb_relay_pong')
) {
window.removeEventListener('message', onMsg);
resolve(frame.contentWindow!);
}
};
window.addEventListener('message', onMsg);
const pingTimer = setInterval(() => {
if(frame.contentWindow) {
frame.contentWindow.postMessage({type: 'vb_relay_ping'}, '*');
}
}, 150);
frame.onload = () => {
clearInterval(pingTimer);
if(frame.contentWindow) {
resolve(frame.contentWindow);
}
};
});
return state.relayPromise;
}
function requestViaRelay(path: string, opts: RequestInit = {}): Promise {
return getRelayWindow().then(relayWin => {
const token = getToken();
const headers: Record = {
'Content-Type': 'application/json',
...(token ? {Authorization: `Bearer ${token}`} : {}),
...(opts.headers as any)
};
const reqId = `req_${Math.random().toString(36).substring(2, 9)}_${Date.now()}`;
return new Promise((resolve, reject) => {
const handler = (e: MessageEvent) => {
if(
!e.data
|| e.data.type !== 'vb_relay_response'
|| e.data.id !== reqId
) {
return;
}
window.removeEventListener('message', handler);
if(e.data.ok) {
resolve(e.data.data as T);
return;
}
if(
e.data.status === 401
&& !(opts as any)._retry
&& !(opts as any)._skipRefresh
&& getRefreshToken()
) {
(opts as any)._retry = true;
refreshToken()
.then(requestViaRelay.bind(null, path, opts))
.then(resolve)
.catch(reject);
return;
}
const err = (e.data.data && e.data.data.error)
|| e.data.error
|| `Relay request failed: ${e.data.status}`;
reject(new Error(err));
};
window.addEventListener('message', handler);
relayWin.postMessage({
type: 'vb_relay_request',
id: reqId,
path,
method: opts.method || 'GET',
headers,
body: opts.body
}, '*');
});
});
}
function request(path: string, opts: RequestInit = {}): Promise {
if(options.useRelay) {
return requestViaRelay(path, opts);
}
const token = getToken();
const headers: Record = {
'Content-Type': 'application/json',
...(token ? {Authorization: `Bearer ${token}`} : {}),
...(opts.headers as Record)
};
return fetch(`${baseUrl}${path}`, {...opts, headers})
.then(res => {
if(
res.status === 401
&& !(opts as any)._retry
&& !(opts as any)._skipRefresh
&& getRefreshToken()
) {
(opts as any)._retry = true;
return refreshToken().then(request.bind(null, path, opts));
}
if(!res.ok) {
return res.json()
.catch(() => ({error: res.statusText}))
.then(err => {
throw new Error(err.error || `HTTP ${res.status}`);
});
}
return res.json();
})
.catch((err: any) => {
if(
err.name === 'TypeError'
&& typeof window !== 'undefined'
&& !options.useRelay
) {
console.warn('[VibeBackr] Direct fetch blocked (CSP/CORS). Falling back to iframe relay...');
return requestViaRelay(path, opts);
}
throw err;
});
}
const login = (provider = 'google', collectionId?: string) =>
new Promise(resolve => {
const width = 500;
const height = 650;
const left = typeof window !== 'undefined' ? (window.screenX || 0) + (window.outerWidth - width) / 2 : 0;
const top = typeof window !== 'undefined' ? (window.screenY || 0) + (window.outerHeight - height) / 2 : 0;
const targetCol = collectionId || defaultCollectionId;
const colParam = targetCol ? `&collection_id=${encodeURIComponent(targetCol)}` : '';
if(typeof window !== 'undefined') {
window.open(
`${baseUrl}/auth/login/${provider}?popup=true${colParam}`,
'vibebackr_login',
`width=${width},height=${height},left=${left},top=${top}`
);
}
const onMessage = (e: any) => {
if(e.data && e.data.access_token) {
if(typeof window !== 'undefined') {
window.removeEventListener('message', onMessage);
}
setToken(e.data.access_token);
if(e.data.refresh_token) {
setRefreshToken(e.data.refresh_token);
}
resolve(e.data);
}
};
if(typeof window !== 'undefined') {
window.addEventListener('message', onMessage);
}
});
const listCollections = () => request('/api/collections');
const listRecords = (colId?: string) =>
request[]>(`/api/collections/${encodeURIComponent(resolveColId(colId))}/records`);
const searchRecords = (colIdOrQuery: string, queryOrLimit?: string | number, limit = 20) => {
let targetCol: string;
let query: string;
let maxResults: number;
if(
defaultCollectionId
&& (queryOrLimit === undefined || typeof queryOrLimit === 'number')
) {
targetCol = defaultCollectionId;
query = colIdOrQuery;
maxResults = typeof queryOrLimit === 'number' ? queryOrLimit : 20;
} else {
targetCol = resolveColId(colIdOrQuery);
query = String(queryOrLimit || '');
maxResults = limit;
}
return request[]>(`/api/collections/${encodeURIComponent(targetCol)}/search`, {
method: 'POST',
body: JSON.stringify({query, limit: maxResults})
});
};
return {
baseUrl,
collectionId: defaultCollectionId,
getToken,
setToken,
getRefreshToken,
setRefreshToken,
refreshToken,
initRelay: getRelayWindow,
login,
loginWithGoogle: login.bind(null, 'google'),
loginWithGitHub: login.bind(null, 'github'),
loginMock: login.bind(null, 'mock'),
getLoginUrl(provider = 'google', redirectUri?: string, collectionId?: string) {
if(!redirectUri && typeof window !== 'undefined') {
const isFile = window.location.protocol === 'file:'
|| window.location.origin === 'null';
redirectUri = isFile ? 'http://localhost:3000' : window.location.origin;
}
const targetCol = collectionId || defaultCollectionId;
const colParam = targetCol ? `&collection_id=${encodeURIComponent(targetCol)}` : '';
return `${baseUrl}/auth/login/${provider}?redirect_uri=${encodeURIComponent(redirectUri || '')}${colParam}`;
},
handleAuthCallback() {
if(typeof window === 'undefined') {
return null;
}
if(window.location.hash.includes('access_token=')) {
const params = new URLSearchParams(window.location.hash.substring(1));
const token = params.get('access_token');
if(token) {
setToken(token);
const refToken = params.get('refresh_token');
if(refToken) {
setRefreshToken(refToken);
}
window.location.hash = '';
return token;
}
}
return getToken();
},
logout() {
const rt = getRefreshToken();
setToken(null);
setRefreshToken(null);
const body = rt ? `refresh_token=${encodeURIComponent(rt)}` : '';
return request('/api/auth/logout', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body,
...({_skipRefresh: true} as any)
}).catch(() => ({message: 'logged out locally'}));
},
revokeAllSessions: () => request('/api/auth/revoke-all', {method: 'POST'}),
listProviders: () => request<{providers: string[]}>('/api/auth/providers').then(r => r.providers || []),
listTenants: () => request('/api/tenants'),
createTenant: () => request('/api/tenants', {method: 'POST'}),
leaveTenant: (tenantId: string) =>
request<{message: string; tenant_id: string}>(`/api/tenants/${encodeURIComponent(tenantId)}`, {method: 'DELETE'}),
listCollections,
getCollections: listCollections,
createCollection: (nameOrConfig: string | Record, defaultVisibility = 'private', originUrls: string[] = []) => {
const payload = (typeof nameOrConfig === 'object' && nameOrConfig !== null)
? nameOrConfig
: {name: nameOrConfig, default_visibility: defaultVisibility, origin_urls: originUrls};
return request('/api/collections', {method: 'POST', body: JSON.stringify(payload)});
},
getCollection: (colId?: string) => request(`/api/collections/${encodeURIComponent(resolveColId(colId))}`),
updateCollection: (colId: string, payload: Record) =>
request(`/api/collections/${encodeURIComponent(colId)}`, {method: 'PUT', body: JSON.stringify(payload)}),
deleteCollection: (colId: string) =>
request<{message: string; id: string}>(`/api/collections/${encodeURIComponent(colId)}`, {method: 'DELETE'}),
listRecords,
getRecords: listRecords,
getRecord: (colIdOrRecId: string, recId?: string) => {
const targetCol = recId === undefined && defaultCollectionId ? defaultCollectionId : resolveColId(colIdOrRecId);
const targetRec = recId === undefined && defaultCollectionId ? colIdOrRecId : recId!;
return request>(`/api/collections/${encodeURIComponent(targetCol)}/records/${encodeURIComponent(targetRec)}`);
},
createRecord: (colIdOrPayload: string | Record, payloadOrColId?: any) => {
const isObj = typeof colIdOrPayload === 'object' && colIdOrPayload !== null;
const targetCol = isObj ? resolveColId(payloadOrColId) : resolveColId(colIdOrPayload as string);
const payload = (isObj ? colIdOrPayload : payloadOrColId) || {};
const accessPolicy = payload.access_policy || {
visibility: payload.visibility || '',
shares: payload.shares || []
};
return request>(`/api/collections/${encodeURIComponent(targetCol)}/records`, {
method: 'POST',
body: JSON.stringify({
title: payload.title,
content: payload.content || '',
data: payload.data !== undefined ? payload.data : {},
access_policy: accessPolicy
})
});
},
updateRecord: (colIdOrRecId: string, recIdOrPayload: string | Record, payloadOrColId?: any) => {
const isSecondObj = typeof recIdOrPayload === 'object' && recIdOrPayload !== null;
const targetCol = isSecondObj ? resolveColId(payloadOrColId) : resolveColId(colIdOrRecId);
const targetRec = isSecondObj ? colIdOrRecId : (recIdOrPayload as string);
const body = {...(isSecondObj ? recIdOrPayload : payloadOrColId)};
if(!body.access_policy && (body.visibility || body.shares)) {
body.access_policy = {visibility: body.visibility || '', shares: body.shares || []};
}
return request>(`/api/collections/${encodeURIComponent(targetCol)}/records/${encodeURIComponent(targetRec)}`, {
method: 'PUT',
body: JSON.stringify(body)
});
},
deleteRecord: (colIdOrRecId: string, recId?: string) => {
const targetCol = recId === undefined && defaultCollectionId ? defaultCollectionId : resolveColId(colIdOrRecId);
const targetRec = recId === undefined && defaultCollectionId ? colIdOrRecId : recId!;
return request<{message: string}>(`/api/collections/${encodeURIComponent(targetCol)}/records/${encodeURIComponent(targetRec)}`, {
method: 'DELETE'
});
},
searchRecords,
search: searchRecords
};
}
```
## Bootstrapping Without MCP (Gemini & ChatGPT)
When working in Google Gemini, AI Studio, or environments without an active MCP connection, you can bootstrap collections and seed records in one step.
### Web Console JSON Import
Open the [Console](/login), click **+ New Collection / Import JSON**, copy the Gemini prompt, and paste the generated JSON back to create the collection and seed records.
### Direct REST API (`POST /api/collections`)
Copy your Personal Access Token from the [Console](/login) and `POST` a JSON payload containing `name`, `default_visibility`, and an optional `records` array.
```json
{
"name": "customer_support",
"default_visibility": "private",
"origin_urls": ["*"],
"vector_search_enabled": true,
"records": [
{
"title": "Welcome Ticket",
"content": "First automated record created by Gemini",
"data": { "status": "open", "priority": "high" }
}
]
}
```
---
# Page: For Nerds - How VibeBackr Works Under the Hood
# How VibeBackr Works Under the Hood
For developers, indie hackers, and curious builders who want to know how VibeBackr turns a single AI prompt into a hosted, multi-user application with zero infrastructure setup.
- [Client Guide (SDK & REST API)](/guide)
- [Self-Hosting Guide](#self-hosting)
- `https://vibebac.kr/mcp`
## Architecture at a Glance
VibeBackr is a single, zero-CGO Go binary that combines a remote Model Context Protocol (MCP) server, a centralized OAuth 2.0 / OIDC broker, a multi-tenant document & vector database, and an edge-hosted single-file HTML application store.
### Single-File Edge App Hosting
When an AI agent calls `publish_app`, VibeBackr validates and stores a self-contained HTML application (up to 512 KB) and serves it from `https://{slug_or_id}.vibebac.kr` via CloudFront & S3 (or local filesystem when self-hosted), automatically wiring the collection's allowed CORS and OAuth origins.
### Centralized OAuth Broker
Instead of requiring users to visit Google Cloud Console or configure redirect URIs, VibeBackr acts as an OAuth broker (supporting Google, GitHub, OIDC, PKCE, and RFC 7591 Dynamic Client Registration). Frontends sign users in with a single `vibe.loginWithGoogle()` call.
### 5-Tier Recursive Waterfall ACLs
Every query evaluates a 5-tier permission waterfall in SQL: **1) Record Ownership**, **2) Manager Hierarchy** (evaluated via recursive CTEs across the tenant's org chart), **3) Public Collection/Domain Visibility**, **4) Explicit Email Shares** (with optional `auto_invite_shares`), and **5) Workspace Admin Override**.
### Universal Records & 768-d Vector RAG
Each record stores a `title`, markdown `content`, and an arbitrary structured JSON `data` payload (up to 64 KB per record and 500 records per collection on managed cloud, uncapped by default when self-hosted). When vector search is enabled, records are indexed with 768-dimensional embeddings (`EmbeddingGemma 2` via `pgvector`) alongside a full-text SQL fallback.
### Zero-Dependency Browser SDK & CSP Relay
The client SDK (`vibebackr.js`) handles token refresh, `file://` null-origin CORS, and automatically falls back to a hidden `/relay` iframe `postMessage` bridge when running inside strict-CSP AI sandboxes (like Claude Artifacts).
### Per-Collection Token-Bucket Rate Limiting
Rate limits are isolated **per individual collection** so one noisy app never starves your other tools: **Standard** allows **60 req/min** (20-request burst), **Pro** allows **180 req/min** (45-request burst), and **VibeBackr OS** runs uncapped on your own hardware.
## Connecting MCP Clients (Claude, Cursor, Copilot)
Connect any Model Context Protocol (MCP) client to `https://vibebac.kr/mcp` over Streamable HTTP (`POST`) or Server-Sent Events (`GET`).
### 1. Claude Desktop & Claude Code
Install in one click with the Claude Desktop MCP Bundle (`.mcpb`), or add VibeBackr to your `claude_desktop_config.json`:
- [Download `vibebackr.mcpb` (One-Click Claude Extension)](/vibebackr.mcpb)
- `https://vibebac.kr/mcp`
```json
{
"mcpServers": {
"vibebackr": {
"url": "https://vibebac.kr/mcp"
}
}
}
```
### 2. Cursor, GitHub Copilot & Other MCP Clients
Add a remote MCP server in your editor settings:
```text
Server URL: https://vibebac.kr/mcp
Transport: Streamable HTTP (POST) or Server-Sent Events (SSE)
Auth: OAuth 2.1 with Dynamic Client Registration (RFC 7591) & PKCE
```
### 3. Available MCP Tools
Once connected, your AI agent has full tool access to provision, populate, publish, and administrate applications:
- `create_collection`: Create an isolated collection (`name`, `slug`, `default_visibility`, `allowed_domains`, `origin_urls`, `vector_search_enabled`, `auto_invite_shares`).
- `update_collection`: Update collection metadata, visibility rules, custom slug, auto-invite settings, or allowed CORS origins.
- `publish_app`: Deploy a live single-file HTML web app for a collection to `https://{slug_or_id}.vibebac.kr`.
- `list_collections`: List all collections in the current tenant workspace.
- `create_record`: Insert a universal record (`collection_id`, `title`, `content`, `data`, `visibility`, `shares`).
- `query_records`: Query records honoring the 5-tier recursive waterfall permission engine.
- `search_records`: Hybrid 768-d semantic vector search & full-text keyword search.
- `sync_org_directory`: Upsert organization hierarchy (`email`, `name`, `role`, `manager_email`) for recursive manager ACL inheritance.
- `batch_create_invites`: Pre-authorize team members with role and manager assignments.
- `get_client_integration_guide`: Retrieve copy-pasteable frontend SDK integration code and tailored TypeScript types.
## Client Guide: Browser SDK & REST API
Building a custom React, Next.js, Vue, Svelte, or vanilla JS frontend - or writing an autonomous agent that interacts directly with the VibeBackr HTTP API? The **[Client Guide](/guide)** covers everything you need:
### Drop-In Browser SDK (`vibebackr.js`)
Zero-dependency ES module and global `