Getting Started
Common Mistakes
The most frequent issues developers encounter when building apps on Visimade, and how to avoid them.
1. Using PageAssets instead of TeamData files
Symptom: Only the page owner can upload files. Team members get 403 errors on upload.
PageAssets (/api/pages/:id/assets) is for static page resources managed by the page owner only. For team_app file uploads where any team member needs to upload, use TeamData.uploadFile() which hits /api/pages/:id/team-files with role-based permissions.
// WRONG - only page owner can upload
const result = await PageAssets.upload(file);
const url = '/api/pages/' + pageId + '/assets/' + result.id;
// CORRECT - any team member (member+) can upload
const result = await TeamData.uploadFile(file);
const url = TeamData.getDownloadUrl(result.id);
// Full file API:
await TeamData.uploadFile(file); // Upload (member+)
const { files } = await TeamData.listFiles(); // List (viewer+)
const url = TeamData.getDownloadUrl(fileId); // Get URL (viewer+)
await TeamData.downloadFile(fileId, { openInNewTab: true }); // Download
await TeamData.deleteFile(fileId); // Delete (creator or admin+)Note: TeamData.uploadFile() returns { id, filename, content_type, size, created_by, created_at } — there is no url property. Always use TeamData.getDownloadUrl(fileId) to get the download URL.
2. Using raw fetch() instead of the injected SDK
Symptom: Duplicated logic, inconsistent error handling, missing auth headers, fragile URL construction.
When your page is served, the platform injects window.TeamData (or SoloData, SocialData) with all CRUD, file, auth, and permission methods built in. Do not write your own fetch wrapper against the REST endpoints — use the SDK directly.
// WRONG - building your own API wrapper
const api = {
async list(col) {
const r = await fetch('/api/pages/' + PAGE_ID + '/team-data/' + col,
{ credentials: 'include' });
return r.json();
},
async create(col, data) {
const r = await fetch('/api/pages/' + PAGE_ID + '/team-data/' + col,
{ method: 'POST', headers: {'Content-Type':'application/json'},
credentials: 'include', body: JSON.stringify({data}) });
return r.json();
}
};
// CORRECT - use the injected SDK
const { records } = await TeamData.find('tasks', { where: { status: 'active' } });
await TeamData.create('tasks', { title: 'New task' });
await TeamData.update('tasks', recordId, { status: 'done' });
await TeamData.delete('tasks', recordId);3. Using wrong SDK method names
Symptom: TypeError: TeamData.list is not a function or similar runtime errors.
The SDK methods are named find, findById, create, update, and delete. While aliases like list, get, remove, and put are supported, always prefer the canonical names. See the Method Reference for the full list.
// WRONG — these are not the canonical method names
await TeamData.list('tasks'); // Use find()
await TeamData.get('tasks', id); // Use findById()
await TeamData.remove('tasks', id); // Use delete()
await TeamData.put('tasks', id, data); // Use update()
await TeamData.getAll('tasks'); // Does not exist
await TeamData.fetchById('tasks', id); // Does not exist
await TeamData.save('tasks', data); // Does not exist
// CORRECT — use the canonical method names
const { records } = await TeamData.find('tasks');
const record = await TeamData.findById('tasks', id);
await TeamData.create('tasks', { title: 'New' });
await TeamData.update('tasks', id, { status: 'done' });
await TeamData.delete('tasks', id);4. Invalid collection names
Symptom: 400 Bad Request on any data operation.
Collection names must match `^[a-z][a-z0-9_]*$` — start with a lowercase letter, contain only lowercase letters, numbers, and underscores, and be at most 100 characters.
// WRONG - starts with underscore
await TeamData.create('_user_probe', { ts: Date.now() });
// WRONG - contains uppercase
await TeamData.find('chatMessages', {});
// WRONG - contains hyphen
await TeamData.find('chat-messages', {});
// CORRECT
await TeamData.create('user_probe', { ts: Date.now() });
await TeamData.find('chat_messages', {});5. Not awaiting TeamData.ready before auth checks
Symptom: getCurrentUser() returns null even when the user is logged in. User always appears logged out.
// WRONG - auth hasn't loaded yet
const user = TeamData.getCurrentUser(); // Always null!
// CORRECT - wait for auth to complete
await TeamData.ready;
const user = TeamData.getCurrentUser(); // Now works correctly
if (!TeamData.isMember()) {
TeamData.promptLogin();
return;
}6. Forgetting storage_mode when creating pages via API
Symptom: TeamData is not defined error when the page loads. The SDK is never injected.
When creating a page via POST /api/pages, the storage_mode defaults to "page" (static, no data APIs). You must explicitly set it to "team_app" for TeamData to be injected.
// WRONG - defaults to storage_mode: "page", no TeamData injected
curl -X POST https://visimade.com/api/pages \
-H "Authorization: Bearer vm_your_token" \
-H "Content-Type: application/json" \
-d '{
"name": "My Team App",
"html_content": "<html>...</html>"
}'
// CORRECT - explicitly set storage_mode
curl -X POST https://visimade.com/api/pages \
-H "Authorization: Bearer vm_your_token" \
-H "Content-Type: application/json" \
-d '{
"name": "My Team App",
"html_content": "<html>...</html>",
"storage_mode": "team_app"
}'7. Inferring user identity from record fields
Symptom: User appears as null until they create their first record. Brittle identity logic.
// WRONG - creates a throwaway record just to read createdBy
const probe = await TeamData.create('user_probe', { ts: Date.now() });
const user = probe.createdBy;
await TeamData.delete('user_probe', probe.id);
// CORRECT - use the auth helpers
await TeamData.ready;
const user = TeamData.getCurrentUser(); // { id, username } or null
const role = TeamData.getRole(); // 'owner' | 'admin' | 'member' | 'viewer' | null
const membership = TeamData.getMembership(); // { userId, role, status } or null8. Not checking permissions before showing UI actions
The server enforces permissions on every request, but your UI should check permissions client-side to avoid showing buttons that will fail when clicked.
// Use permission helpers to conditionally show UI
if (TeamData.canCreate()) {
showNewRecordButton();
}
if (TeamData.canEdit(record)) {
showEditButton();
}
if (TeamData.canDelete(record)) {
showDeleteButton();
}
if (TeamData.canInvite()) {
showInviteMemberButton();
}
if (TeamData.canManageRoles()) {
showRoleManagementUI();
}9. Assuming deep merge on update / batch update
Both PATCH and batch update operations perform a shallow merge of the data field. Top-level fields you omit are preserved, but nested objects are fully replaced — not deep-merged.
// Existing record:
// { "title": "My Post", "meta": { "author": "Alice", "tags": ["news"] } }
// ❌ WRONG — this replaces the entire "meta" object, losing "author"
await TeamData.update('posts', id, { meta: { tags: ["news", "update"] } });
// Result: { "title": "My Post", "meta": { "tags": ["news", "update"] } }
// ^^^ "author" is gone!
// ✅ CORRECT — fetch first, modify, send back
const record = await TeamData.findById('posts', id);
const updatedMeta = { ...record.data.meta, tags: ["news", "update"] };
await TeamData.update('posts', id, { meta: updatedMeta });
// Result: { "title": "My Post", "meta": { "author": "Alice", "tags": ["news", "update"] } }The same applies to batch operations. If you only need to change one top-level field (e.g., order), you can safely send just that field. But if you need to update a nested property, fetch the record first and send the complete nested object back.
On this page
- 1. PageAssets vs TeamData
- 2. Raw fetch() vs SDK
- 3. Wrong SDK Method Names
- 4. Invalid Collection Names
- 5. Not Awaiting .ready
- 6. Forgetting storage_mode
- 7. Inferring User Identity
- 8. Permission Checks
- 9. Shallow Merge on Update