@mindstudio-ai/remy 0.1.268 → 0.1.270

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,437 +1,42 @@
1
1
  # Auth
2
2
 
3
- Remy apps can have and manage their own users. Auth is opt-in: configure it in the manifest, define a user table, and build your own login UI. The platform handles verification codes, cookies, and session management. Apps without auth config use anonymous guest sessions (current default behavior).
3
+ Remy apps can have and manage their own users. Auth is opt-in: configure it in the manifest, define a user table, and build your own login UI. The platform handles verification codes, cookie sessions, and role enforcement. Apps without auth config use anonymous guest sessions — the default, and fine for single-user apps, internal utilities, and simple tools.
4
4
 
5
5
  **Auth is optional.** Many apps don't need it. Only add auth when the app needs to identify users or restrict access.
6
6
 
7
- ## Manifest Config
7
+ Four auth methods, combinable per app in the manifest:
8
8
 
9
- ```json
10
- {
11
- "auth": {
12
- "enabled": true,
13
- "methods": ["email-code", "sms-code"],
14
- "table": {
15
- "name": "users",
16
- "columns": {
17
- "email": "email",
18
- "phone": "phone",
19
- "roles": "roles",
20
- "apiKey": "apiKey"
21
- }
22
- }
23
- },
24
- "roles": [
25
- { "id": "vendor", "name": "Vendor" },
26
- { "id": "buyer", "name": "Buyer" },
27
- { "id": "admin", "name": "Admin" }
28
- ]
29
- }
30
- ```
31
-
32
- - **`auth.enabled`** — opt-in. No auth config = anonymous guest sessions.
33
- - **`auth.methods`** — which verification methods the app supports. At least one required.
34
- - `email-code` — 6-digit code sent via email
35
- - `sms-code` — 6-digit code sent via SMS
36
- - `api-key` — programmatic access via `Authorization: Bearer sk_...` header. Resolves to a user with full RBAC.
37
- - `remy` — platform-delegated sign-in ("Sign in with Remy"). The platform resolves who the user is; roles and verification are platform-managed. Only usable when the app's owning organization has it enabled — the `<org_context>` block signals availability. See *Organization-Managed Sign-In* above.
38
- - **`auth.table.name`** — name of the `defineTable` table that holds user records.
39
- - **`auth.table.columns`** — maps platform-managed fields to column names in the developer's table.
40
- - `email` — required if `email-code` is in methods
41
- - `phone` — required if `sms-code` is in methods
42
- - `roles` — optional. Maps to a JSON array column for role assignments.
43
- - `apiKey` — optional. Required if `api-key` is in methods. Platform stores masked value (`sk_...xxxx`) for display; one key per user.
44
- - **`roles`** — declares valid roles for the app. Same as before: `id`, `name`, optional `description`.
9
+ - `email-code` — 6-digit code sent via email. The natural default for business/desktop apps.
10
+ - `sms-code` — 6-digit code sent via SMS. The natural default for consumer/mobile apps.
11
+ - `api-key` — programmatic access via `Authorization: Bearer sk_...`; resolves to a user with full RBAC.
12
+ - `remy` — org-delegated "Sign in with Remy" (see below). Internal apps only; org-gated.
45
13
 
46
- ## Auth Table
14
+ **Before writing any auth code — the manifest `auth` config, the user table, login/signup UI, frontend `auth.*` calls, API keys — load the `auth` skill.** It has the full contract: config schema, platform-managed columns, the frontend SDK (flows, auth state, error codes, phone/email helpers), delegated sign-in implementation, worked examples, and the auth-screen design rules.
47
15
 
48
- The user table is a regular `defineTable` table. The platform manages the auth-mapped columns; all other columns are the developer's domain.
16
+ ## Backend Enforcement
49
17
 
50
18
  ```typescript
51
- import { db } from '@mindstudio-ai/agent';
52
-
53
- export const Users = db.defineTable<{
54
- // Mapped to auth — platform keeps these in sync
55
- email: string;
56
- phone?: string;
57
- roles: string[];
58
- apiKey?: string; // masked value (sk_...xxxx), read-only from code
59
- // Developer's own fields
60
- displayName: string;
61
- plan: 'free' | 'pro';
62
- avatarUrl?: string;
63
- }>('users');
64
- ```
65
-
66
- ### Platform-Managed Column Behavior
67
-
68
- - **`email` / `phone` / `apiKey`** — read-only from code. Writing via `update()` or `push()` throws a `MindStudioError`. Use the auth API to change a user's email or phone, and `auth.createApiKey()` / `auth.revokeApiKey()` for API keys.
69
- - **`roles`** — read/write from both code and the dashboard. `Users.update(userId, { roles: ['admin'] })` works and syncs to the platform. Dashboard role changes sync back to the table.
70
- - All other columns are fully the developer's. When auth creates a user row, only the managed columns (email/phone, roles) are populated. All user-defined columns start as null until the user completes onboarding — type them as optional and guard against null.
71
-
72
- ## Frontend Auth (Interface SDK)
73
-
74
- The developer builds their own login/signup UI. The SDK provides methods that handle the verification flow and session management.
75
-
76
- ```typescript
77
- import { auth } from '@mindstudio-ai/interface';
78
- ```
79
-
80
- ### User Shape
81
-
82
- ```typescript
83
- interface AppUser {
84
- id: string;
85
- email: string | null;
86
- phone: string | null;
87
- roles: string[];
88
- apiKey: string | null; // masked value (sk_...xxxx), null if no key
89
- provider?: 'remy' | null; // 'remy' = delegated sign-in (platform-managed); null/absent = app-verified
90
- createdAt: string;
91
- }
92
- ```
93
-
94
- `auth.getCurrentUser()` returns `AppUser | null`. `null` means unauthenticated.
95
-
96
- ### State
97
-
98
- ```typescript
99
- auth.getCurrentUser() // AppUser | null
100
- auth.currentUser // AppUser | null (sync getter, same as getCurrentUser())
101
- auth.isAuthenticated() // boolean
102
- auth.authStatus // 'authenticating' | 'authenticated' | 'unauthenticated' (sync getter)
103
- auth.onAuthStateChanged(cb) // fires immediately with current user, then on every transition —
104
- // verify/confirm/logout AND each authStatus change (a Sign in with Remy
105
- // handshake starting or settling). During 'authenticating' it may fire
106
- // null; that's expected — read authStatus to tell it apart from
107
- // logged-out. Returns an unsubscribe function.
108
- ```
109
-
110
- Use `onAuthStateChanged` in React instead of reading `currentUser` once at render time:
111
-
112
- ```typescript
113
- function useAuth() {
114
- const [user, setUser] = useState<AppUser | null>(null);
115
- useEffect(() => auth.onAuthStateChanged(setUser), []);
116
- return user;
117
- }
118
- ```
119
-
120
- ### Email Code Flow
121
-
122
- ```typescript
123
- const { verificationId } = await auth.sendEmailCode('user@example.com');
124
- // User enters the 6-digit code from their email
125
- const user = await auth.verifyEmailCode(verificationId, '123456');
126
- // user is now authenticated — auth.getCurrentUser() returns the AppUser
19
+ import { auth } from '@mindstudio-ai/agent';
127
20
  ```
128
21
 
129
- ### SMS Code Flow
130
-
131
- ```typescript
132
- const { verificationId } = await auth.sendSmsCode('+15551234567');
133
- const user = await auth.verifySmsCode(verificationId, '123456');
134
- ```
22
+ - **`auth.userId`** — the current user's ID (row ID in the auth table), or `null` for unauthenticated requests. Check for null before using in queries when the method might be called without auth.
23
+ - **`auth.requireRole(...roles)`** — throws 401 if unauthenticated, 403 if the user has none of the listed roles (OR semantics). `auth.hasRole(...roles)` is the non-throwing boolean form. `auth.roles` is the current user's role array.
24
+ - **Require login: check `auth.userId`. Roles are RBAC** — only declare roles that map to real business distinctions (vendor/buyer/admin), and only check them when behavior should differ. Newly verified users have `roles: []` until your code assigns them.
25
+ - **System role:** when the platform invokes a method on behalf of the app (cron, webhook, email), execution runs with `auth.roles: ['system']` — use `auth.requireRole('system')` to restrict a method to platform triggers. Web frontend, API, and agent calls run as the authenticated user and never get the system role unless explicitly assigned.
135
26
 
136
27
  ## Organization-Managed Sign-In ("Sign in with Remy")
137
28
 
138
- Some apps are owned by an organization that centralizes sign-in. When that applies to the current app, the system prompt includes an `<org_context>` block (near the end); for org-managed sign-in it states the organization name and whether delegated sign-in is available. Sign in with Remy is a way to make authentication seamless for internal apps - it should not be used for public-facing applications. The app owner will need to add users to their workspace's team on the Remy platform and those users will need Remy accounts for it to work.
29
+ Some apps are owned by an organization that centralizes sign-in. The `<org_context>` block (near the end of this prompt) states the facts; here is how to act on them:
139
30
 
140
- - **When it says "Sign in with Remy" is available** — offer delegated sign-in: a **"Continue with {Org}"** button wired to the `remy` method (see *Sign in with Remy (delegated)* below). Use the exact organization name from the block for the label. For an org-owned app this is usually the primary sign-in — the members already have platform identities, so a verification-code form is redundant.
31
+ - **When it says "Sign in with Remy" is available** — offer delegated sign-in: a "Continue with {Org}" button (exact organization name from the block). For an org-owned app this is usually the primary sign-in — members already have platform identities, so a verification-code form is redundant.
141
32
  - **When it says the organization requires delegated sign-in** — `remy` is the *only* human method: do not add `email-code` or `sms-code`. Those are blocked at the platform edge for the org's apps, so building them yields a login that can't work.
33
+ - **When the block is absent, or has no delegated-sign-in line** (the common case) — do not build or offer it. It's an enterprise scheme for internal apps only.
34
+ - Delegated users' **roles and email are platform-managed** — enforce with `requireRole`/`hasRole` as usual, but never assign their roles from app code.
142
35
 
143
- When the `<org_context>` block is absent, or present without a delegated-sign-in line — the common case — do not build it or offer to build it. This is an auth scheme for enterprises building internal apps only, and it requires a Remy enterprise plan to use. When enabled, the platform decides who the user is (like "Sign in with Google"); the app just starts the flow and reads the result.
144
-
145
- ```typescript
146
- // "Continue with {Org}" button — must be triggered by a user gesture (click).
147
- <button onClick={() => auth.signInWithRemy()}>Continue with Acme</button>
148
-
149
- // On return from the handshake (or when opened from the Remy dashboard), the app
150
- // cold-loads with sign-in still completing. Track authStatus and render a
151
- // "Completing sign-in…" state — NOT the login screen — while it's 'authenticating'.
152
- const [user, setUser] = useState(auth.currentUser);
153
- const [status, setStatus] = useState(auth.authStatus);
154
- useEffect(() => auth.onAuthStateChanged(() => {
155
- setUser(auth.currentUser);
156
- setStatus(auth.authStatus);
157
- }), []);
158
- useEffect(() => { auth.handleRemyRedirect().catch(() => {}); }, []); // no-op if nothing to redeem
159
- // status === 'authenticating' → <CompletingSignIn/>; user → app; else → login
160
- ```
161
-
162
- - `auth.signInWithRemy(options?)` → `Promise<AppUser | null>`. Auto-detects context: a top-level app redirects to the platform and back — **the page navigates away, so the promise never settles; don't await it to gate UI**. An app embedded in a cross-origin iframe (the dev IDE preview) uses a popup and the promise resolves with the user (or `null` if the popup is closed). Options: `redirectUri` (default current URL), `state` (CSRF, auto-generated), `mode: 'auto' | 'popup' | 'redirect'` (default `'auto'` — leave it). Because of the redirect case, **drive UI off `onAuthStateChanged`, not the return value.**
163
- - `auth.handleRemyRedirect()` → `Promise<AppUser | null>`. Call once on load. Handles both the "Continue with {Org}" return and being opened from the Remy dashboard; on success it updates the session in-place (fires `onAuthStateChanged`) and cleans the URL.
164
- - **Render the completing state, not the login screen, on return.** A redirect return (or dashboard launch) cold-loads with `auth.authStatus === 'authenticating'` — set *before first paint* and held until the exchange settles. Gate the UI on `authStatus`: `'authenticating'` → a brief "Completing sign-in…" state; a user → the app; `'unauthenticated'` → the login screen. Don't infer this from the URL `?code` (it's stripped when redemption starts) or treat the initial `null` user as logged-out — that flashes the login screen over a successful sign-in.
165
- - Delegated users have `provider: 'remy'`. Their **roles and email are platform-managed** (like `email`/`phone` for code users) — enforce access with `requireRole`/`hasRole` on the backend as usual, but don't assign roles from app code; the platform owns them.
166
-
167
- ### Email/Phone Changes (must be authenticated)
168
-
169
- ```typescript
170
- await auth.requestEmailChange('newemail@example.com');
171
- const user = await auth.confirmEmailChange('newemail@example.com', '123456');
172
-
173
- await auth.requestPhoneChange('+15559876543');
174
- const user = await auth.confirmPhoneChange('+15559876543', '123456');
175
- ```
176
-
177
- ### Logout
36
+ ## Designing Auth Into the Experience
178
37
 
179
- ```typescript
180
- await auth.logout(); // clears session
181
- ```
182
-
183
- ### API Keys
184
-
185
- For apps with `api-key` in their auth methods. API keys resolve to a user with the same `auth.userId`, `auth.roles`, and `requireRole()` enforcement as a cookie session.
186
-
187
- ```typescript
188
- // Generate a key for the current user (must be logged in)
189
- const { key } = await auth.createApiKey();
190
- // key = "sk_..." — show once, not stored. user.apiKey updates to masked value.
191
-
192
- // Revoke current user's key
193
- await auth.revokeApiKey();
194
- // user.apiKey becomes null
195
- ```
196
-
197
- Both methods fire `onAuthStateChanged` since they modify the user object. One key per user — creating a new key replaces the old one.
198
-
199
- Consumers use the key as a Bearer token: `Authorization: Bearer sk_...`. The platform resolves it to the user, populates the auth context, and executes the method normally. Invalid or revoked key: 401.
200
-
201
- ### Error Codes
202
-
203
- All auth methods throw on failure with a `code` property:
204
-
205
- | Code | HTTP | Meaning |
206
- |------|------|---------|
207
- | `rate_limited` | 429 | Too many requests |
208
- | `invalid_code` | 400 | Wrong verification code |
209
- | `verification_expired` | 400 | Code has expired |
210
- | `max_attempts_exceeded` | 400 | Too many failed attempts |
211
- | `not_authenticated` | 401 | No active session |
212
- | `invalid_session` | 401 | Session expired or invalid |
213
- | `not_supported` | 400 | Feature not enabled for this app (e.g. API keys without `api-key` in methods) |
214
- | `invalid_state` | 400 | Sign in with Remy: returned CSRF state didn't match (stale/replayed redirect) |
215
- | `popup_blocked` | — | Sign in with Remy (embedded): popup was blocked — prompt to allow popups and retry |
216
- | `signin_timeout` | — | Sign in with Remy (embedded): popup didn't complete in time |
217
- | `auth_required` | 401 | Agent/voice interface requires an authenticated user (interface `auth` block) |
218
- | `role_required` | 403 | Agent/voice interface requires a role the user doesn't hold |
219
-
220
- ### Phone Helpers
221
-
222
- ```typescript
223
- auth.phone.countries // ~180 countries with { code, dialCode, name, flag }
224
- // Key selects by country code (US, CA, BB), not dial code — multiple countries share +1
225
- auth.phone.detectCountry() // guess from timezone, e.g. 'US'
226
- auth.phone.toE164('5551234567', 'US') // '+15551234567'
227
- auth.phone.format('+15551234567') // '+1 (555) 123-4567'
228
- auth.phone.isValid('+15551234567') // true
229
- ```
230
-
231
- ### Email Helpers
232
-
233
- ```typescript
234
- auth.email.isValid('user@example.com') // true
235
- ```
236
-
237
- ## Backend Auth (Agent SDK)
238
-
239
- ```typescript
240
- import { auth } from '@mindstudio-ai/agent';
241
- ```
242
-
243
- ### `auth.requireRole(...roles)`
244
-
245
- Throws 401 (unauthenticated) if there is no current user. Throws 403 (forbidden) if the current user doesn't have **any** of the specified roles.
246
-
247
- ```typescript
248
- auth.requireRole('admin');
249
- auth.requireRole('admin', 'approver'); // any of these
250
- ```
251
-
252
- **Require login: check `auth.userId`. Roles are RBAC** — only declare roles that map to real business distinctions (vendor/buyer/admin), and only check them when behavior should differ. Newly verified users have `roles: []` until your code assigns them.
253
-
254
- ### `auth.hasRole(...roles)`
255
-
256
- Returns `boolean`. Same logic as `requireRole` but doesn't throw.
257
-
258
- ### `auth.userId`
259
-
260
- The current user's ID (the row ID in the auth table), or `null` for unauthenticated requests. Check for null before using in database queries when the method might be called without auth.
261
-
262
- ### `auth.roles`
263
-
264
- Array of role IDs assigned to the current user.
265
-
266
- ### `auth.getUsersByRole(role)`
267
-
268
- Returns an array of user IDs with the specified role.
269
-
270
- ### System Role (Platform Triggers)
271
-
272
- When the platform invokes a method on behalf of the app (cron, webhook, email), the execution runs as a system user with `auth.roles: ['system']`. Use `auth.requireRole('system')` to restrict methods to platform triggers only:
273
-
274
- ```typescript
275
- export async function regenerateCache(input: {}) {
276
- auth.requireRole('system');
277
- // Only cron, webhooks, and users with 'system' role can reach this
278
- }
279
- ```
280
-
281
- Web frontend calls (`/_/methods`), API interface calls (`/_/api`), and agent chat all run as the authenticated user — they don't get the system role unless the user has been explicitly assigned it. You can assign `system` to app users via the dashboard or SDK if they need to manually trigger these methods.
282
-
283
- ## Login Page Example
284
-
285
- ```tsx
286
- import { useState, useEffect } from 'react';
287
- import { auth } from '@mindstudio-ai/interface';
288
- import { useLocation } from 'wouter';
289
-
290
- function useAuth() {
291
- const [user, setUser] = useState<AppUser | null>(null);
292
- useEffect(() => auth.onAuthStateChanged(setUser), []);
293
- return user;
294
- }
295
-
296
- function LoginPage() {
297
- const user = useAuth();
298
- const [, navigate] = useLocation();
299
- const [email, setEmail] = useState('');
300
- const [code, setCode] = useState('');
301
- const [verificationId, setVerificationId] = useState('');
302
- const [error, setError] = useState('');
303
-
304
- // Redirect when authenticated (fires via onAuthStateChanged after verify)
305
- useEffect(() => { if (user) navigate('/dashboard'); }, [user]);
306
-
307
- const handleSendCode = async () => {
308
- try {
309
- const { verificationId } = await auth.sendEmailCode(email);
310
- setVerificationId(verificationId);
311
- setError('');
312
- } catch (err: any) {
313
- setError(err.code === 'rate_limited' ? 'Too many attempts. Try again later.' : err.message);
314
- }
315
- };
316
-
317
- const handleVerify = async () => {
318
- try {
319
- await auth.verifyEmailCode(verificationId, code);
320
- // onAuthStateChanged fires, useAuth updates, redirect happens
321
- } catch (err: any) {
322
- if (err.code === 'invalid_code') setError('Wrong code. Try again.');
323
- else if (err.code === 'verification_expired') setError('Code expired. Request a new one.');
324
- else if (err.code === 'max_attempts_exceeded') setError('Too many attempts. Request a new code.');
325
- else setError(err.message);
326
- }
327
- };
328
-
329
- if (!verificationId) {
330
- return (
331
- <div>
332
- <h1>Sign in</h1>
333
- <input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} />
334
- <button onClick={handleSendCode}>Send code</button>
335
- {error && <p>{error}</p>}
336
- </div>
337
- );
338
- }
339
-
340
- return (
341
- <div>
342
- <p>Enter the code we sent to {email}</p>
343
- <input placeholder="123456" value={code} onChange={e => setCode(e.target.value)} />
344
- <button onClick={handleVerify}>Verify</button>
345
- <button onClick={() => setVerificationId('')}>Resend</button>
346
- {error && <p>{error}</p>}
347
- </div>
348
- );
349
- }
350
- ```
351
-
352
- ## Backend Method Example
353
-
354
- ```typescript
355
- import { auth } from '@mindstudio-ai/agent';
356
- import { Users } from './tables/users';
357
-
358
- export async function getDashboard() {
359
- const user = auth.userId ? await Users.get(auth.userId) : null;
360
-
361
- if (auth.hasRole('admin')) {
362
- const allUsers = await Users.toArray();
363
- return { user, allUsers, isAdmin: true };
364
- }
365
-
366
- return { user, isAdmin: false };
367
- }
368
-
369
- export async function promoteToAdmin(input: { userId: string }) {
370
- auth.requireRole('admin');
371
- await Users.update(input.userId, { roles: ['admin'] });
372
- }
373
- ```
374
-
375
- ## Roles
376
-
377
- Roles are declared in the manifest, stored as an array column on the user table, and enforced in backend methods. The platform manages role data across the user table and the dashboard.
378
-
379
- - Declare roles in `mindstudio.json` with `id` and `name`
380
- - The mapped `roles` column holds a JSON array of role ID strings: `["vendor", "admin"]`
381
- - Writable from code: `Users.update(userId, { roles: ['admin'] })` — platform syncs automatically
382
- - Writable from dashboard: Remy dashboard shows app users and their roles
383
- - Backend enforcement: `auth.requireRole('admin')` works as before
384
-
385
- ## Interface-Level Auth (Agent + Voice)
386
-
387
- Agent and voice interfaces additionally declare auth **in their config** (a required `auth` key:
388
- `{ "requireUser": boolean, "requireRole"?: string[] }`) because those sessions spend money without
389
- necessarily calling a backend method — the platform gates the lobby itself, before any model or
390
- media spend. `requireRole` uses the same manifest role ids with OR semantics. Denials reach the
391
- frontend SDK as `MindStudioInterfaceError` codes `auth_required` (401) and `role_required` (403) —
392
- route them to the app's login flow. See the `agentInterfaces` / `voiceInterfaces` skills for the
393
- full contract. Method-level `auth.requireRole(...)` checks still apply to every tool call inside.
394
-
395
- ## Apps Without Auth
396
-
397
- Apps without `auth` in the manifest use anonymous guest sessions. No login, no user identity, no roles. This is the default and works fine for single-user apps, internal tools, and simple utilities. (Agent/voice interfaces on such apps declare `"auth": { "requireUser": false }` explicitly — anonymous callers are scoped by a per-browser visitor identity.)
398
-
399
- ## Important: Designing Auth in Web Interfaces
400
-
401
- The most imporant user experience consideration with auth is that authentication moments must feel natural and intuitive - they should not feel jarring or surprising. Take care to integrate them into the entire experience when building.
402
-
403
- For the overwhelming majority of apps, a user should never land on auth at the root of an app when opening it for the first time (except in cases where the app is, e.g., an internal tool or some other protected experience - and even then it should feel more like a welcome/splash screen than an error state). Users should be able to explore public resources, or at least encounter some kind of landing/introduction moment, before they get hit with a signup/login screen. Make auth feel like a natural moment in the user's journey.
404
-
405
- Login and signup screens set the tone for the user's entire experience with the app and are important to get right - they should feel like exciting entry points into the next level of the user journy. A janky login form with misaligned inputs and no feedback dminishes excitement and undermines trust before the user even gets in.
406
-
407
- Login and signup are separate moments - even if the underlying code is the same for both. A new user signing up should feel like they are creating a new account. A user logging in should feel like they are being welcomed back. Auth is always a pain for users, even when it's as frictionless as this, so take care to use Sign Up screens as moments to communicate value and help the user get excited about what they are joining.
408
-
409
- Consult the `visualDesignExpert` to help you work through authentication at a high level, including when and where to show auth, and the design of specific screens.
410
-
411
- ### Rules for Building Auth Screens
412
- **Auth modes:** Think about which mode(s) makes the most sense for the type of app you are building. Consumer apps likely to be used on mobile should probably tend toward SMS auth as the default - business apps used on desktop make more sense to use email verification - or allow both, there's no harm in giving the user choice!
413
-
414
- **"Continue with {Org}" (delegated):** When the `<org_context>` block says delegated sign-in is available, a single "Continue with {Org}" button is the primary path — often the *only* one — and there's no verification-code step to design at all (the platform handles it). Give the button real weight in the branded login moment rather than treating it as a secondary option, and use the exact organization name. If the org also allows code methods, delegated goes first with the code form beneath. On return from the handshake (and on dashboard launch), render a brief "Completing sign-in…" state driven by `auth.authStatus === 'authenticating'` — never the login form — so a successful sign-in doesn't flash the logged-out screen.
415
-
416
- **Verification code input:** The 6-digit code entry is the critical moment. Prefer to design it as individual digit boxes (not a single text input), with auto-advance between digits, a beautiful animation and auto-submit on paste, and clear visual feedback. The boxes should be large enough to tap easily on mobile. Show a subtle animation on successful verification. Error states should be inline and immediate, not a separate alert. Make sure there is no layout shift when loading in the success/error states - loading spinners must never pop in below the input and shift the content, for example.
417
-
418
- **The send/resend flow:** After the user enters their email or phone and taps "Send code," show clear confirmation that the code was sent ("Check your email" with the address displayed). Include a resend option with a cooldown timer (e.g., "Resend in 30s"). The transition from "enter email/phone" to "enter code" should feel smooth, not like a page reload. Always make sure the user can cancel and exit the flow (e.g., they had a typo in their email, or remembered they used a different email to sign up).
419
-
420
- **The overall login page:** This is a branding moment. Use the app's full visual identity — colors, typography, any logos, hero imagery, or illustration. A centered card on a branded background is a classic pattern. Don't make it look like a generic SaaS login template. The login page must feel like it belongs to this specific app. Consult the `visualDesignExpert` for guidance on how to really make this shine.
421
-
422
- **Post-login transition:** After successful verification, the transition into the app should feel seamless and instant. Avoid a blank loading screen — if data needs to load, show the app shell with skeleton states. Always make sure the user has a way of logging out.
38
+ Authentication moments must feel natural and intuitive — never jarring or surprising. For the overwhelming majority of apps, a user should never land on auth at the root of the app on first open: let them explore public resources or meet a landing/introduction moment first (an internal tool can gate at the root, but as a welcome/splash, not an error state). Login and signup are separate moments even when the code is shared — signup should communicate value and build excitement about what the user is joining; login should feel like being welcomed back. The login page is a branding moment: the app's full visual identity, not a generic SaaS template. Consult the `visualDesignExpert` on when and where auth appears in the journey and on the screens themselves; the `auth` skill carries the concrete screen-building rules.
423
39
 
424
40
  ## Testing Auth in Development
425
41
 
426
- Auth works the same in dev/preview as in production — real verification codes are sent to real email addresses and phone numbers. There are two test bypasses:
427
-
428
- - **Email:** `remy@mindstudio.ai` — verification code is always `123456`
429
- - **Phone:** any `555` number (e.g. `+15551234567`) — verification code is always `123456`
430
-
431
- All other emails and phone numbers receive real codes. There is no dev-mode bypass, no fake code, and no way to skip verification. When testing auth flows in the preview, use one of the test bypasses above or a real email/phone.
432
-
433
- The `runMethod` tool's `userId: "testUser"` shortcut resolves to this same dev-bypass identity. The platform find-or-creates a real users-table row for it on first call and caches the row's UUID for the rest of the dev session. **`auth.userId` inside the method is that UUID — not the literal string `"testUser"`.** The user row already exists, so don't try to insert it. If you need the UUID to seed app-specific rows that reference it (profiles, preferences, foreign keys), read it from any method response or query the users table directly: `SELECT id FROM users WHERE email = 'remy@mindstudio.ai'` (or `phone = '+15555555555'` for SMS-auth apps).
434
-
435
- For **"Sign in with Remy"** apps (`auth.methods` is `["remy"]`, with no `email-code`/`sms-code`), `testUser` — and `setupBrowser` — resolve to **the developer's own delegated Remy identity**, not the `remy@mindstudio.ai` code-bypass user. `auth.userId` is still that user's real UUID, but the `remy@mindstudio.ai` email lookup above does not apply — read the UUID from a method response instead.
436
-
437
- Browser automation tools (screenshots, automated browser tests) handle their own auth sessions. Scenarios seed database data but do not create browser auth sessions.
42
+ Real verification codes are sent to real addresses in dev — the only bypasses are `remy@mindstudio.ai` (email) and any `555` phone number, both with fixed code `123456`. There is no other fake code or skip. `runMethod`'s `userId: "testUser"` resolves to a real users-table row for that same identity — **`auth.userId` inside the method is that row's UUID, not the literal string `"testUser"`**, and the row already exists (don't insert it). Full detail, including the "Sign in with Remy" variant, is in the `auth` skill.
@@ -53,6 +53,8 @@ Every interface must work on both desktop and mobile. Think about how the app wi
53
53
 
54
54
  The `designExpert` can create and source amazing, high quality images, graphics, illustrations, and logos to use in the interface - both with and without transparency. This is a huge level for upgrading the premium look, feel, and quality of the app. Use image logos directly instead of plain text wordmarks; use images for empty states, onboarding screens, full-screen loading, and more.
55
55
 
56
+ Public CDN images accept resize query params — request the size the layout actually displays (e.g. `?w=400&dpr=2` so it stays crisp on Retina) instead of CSS-scaling a full-res original. The full parameter table is in the `files` skill.
57
+
56
58
  ## Forms
57
59
 
58
60
  Forms should feel like interactions, not paperwork.
@@ -82,8 +84,8 @@ Buttons should use a small animated spinner during loading, not text labels like
82
84
  The UI should feel instant. Never make the user wait for a server round-trip to see the result of their own action. Consider loading a bunch of data in one API call, rather than a bunch of small calls (e.g., if loading a post, also preload comments, likes, user artifacts, etc - don't use separate API calls for each GET).
83
85
 
84
86
  - **Optimistic updates.** When a user adds a row, toggles a setting, or submits a form, update the UI immediately and let the backend confirm in the background. If the backend fails, revert and show an error.
85
- - **Use SWR for data fetching** (`useSWR` from the `swr` package). It handles caching, revalidation, and stale-while-revalidate out of the box. Prefer SWR over manual `useEffect` + `useState` fetch patterns.
86
- - **Mutate after actions.** After a successful create/update/delete, call `mutate()` to revalidate the relevant SWR cache rather than manually updating local state.
87
+ - **Data fetching.** For apps that can load everything on startup, use a Zustand store with a single initial fetch (see the coding instructions) — SWR (`useSWR`) is for server-cache patterns where data is too large or dynamic to hold in memory. Whichever fits, never leave fetches as ad-hoc `useEffect` + `useState` patterns.
88
+ - **Mutate after actions.** After a successful create/update/delete, update the store optimistically (or call `mutate()` to revalidate the relevant cache when using SWR) rather than refetching everything.
87
89
  - **Skeleton loading.** Show subtle, simple skeletons (light pulse - no shimmer) that mirror the layout on initial load. Never show a blank page or centered spinner while data is loading.
88
90
 
89
91
  ### Errors
@@ -48,14 +48,7 @@ Every live deploy runs an automated Lighthouse audit of the app. Pull it via `mi
48
48
 
49
49
  ### Database Migrations on Deploy
50
50
 
51
- Schema changes are automatic:
52
- - **New tables** — `CREATE TABLE` applied automatically
53
- - **New columns** — `ALTER TABLE ADD COLUMN` applied automatically
54
- - **Dropped columns** — `ALTER TABLE DROP COLUMN` applied automatically when a column is removed from the interface
55
- - **Dropped tables** — `DROP TABLE` applied automatically when a table file is removed from the manifest
56
- - **Type changes and renames** — not supported in the automatic migration path
57
-
58
- Schema changes are always applied to a clone of the live database, never directly. If DDL fails, the live database is untouched and the release is marked `failed`.
51
+ Schema changes are automatic — adds and drops of tables/columns are diffed from the table definitions and applied as DDL (type changes and renames are not supported; full rules in the Tables docs). Changes are always applied to a clone of the live database, never directly. If DDL fails, the live database is untouched and the release is marked `failed`.
59
52
 
60
53
  ### Rollback
61
54
 
@@ -61,7 +61,7 @@ const api = createClient<{
61
61
  const { vendorId } = await api.submitVendorRequest({ name: 'Acme' });
62
62
  const { vendors } = await api.listVendors();
63
63
 
64
- // File upload → client-direct to the app's file store (see Files & Storage).
64
+ // File upload → client-direct to the app's file store (load the `files` skill for the backend side).
65
65
  // A backend method mints an upload token; the browser uploads straight to storage.
66
66
  const token = await api.getUploadSlot({ filename: file.name, contentType: file.type });
67
67
  const { key, url } = await platform.upload(token, file);
@@ -79,11 +79,9 @@ auth.getCurrentUser() // AppUser { id, email, phone, roles, create
79
79
  auth.currentUser // same as getCurrentUser() (sync getter)
80
80
  auth.isAuthenticated() // boolean
81
81
  auth.onAuthStateChanged(cb) // fires immediately + on transitions; returns unsubscribe
82
- auth.sendEmailCode(email) // → { verificationId }
83
- auth.verifyEmailCode(verId, code) // → AppUser (sets session)
84
- auth.sendSmsCode(phone) // → { verificationId }
85
- auth.verifySmsCode(verId, code) // → AppUser (sets session)
86
82
  auth.logout() // clears session
83
+ // Verification flows (send/verify email + SMS codes, delegated sign-in, API keys)
84
+ // are in the `auth` skill — load it before building login/signup.
87
85
  ```
88
86
 
89
87
  For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. Load the `agentInterfaces` skill for its usage — thread APIs, streaming callbacks, and attachments are all there.
@@ -154,43 +152,27 @@ Each has its own skill carrying the config shape, the input the method receives,
154
152
 
155
153
  ## Cron
156
154
 
157
- Scheduled method execution — a method plus a cron expression, synced to the platform on deploy.
158
-
159
- **Load the `scheduledJobs` skill** before adding one.
155
+ Scheduled method execution — a method plus a cron expression, synced to the platform on deploy. **Load the `scheduledJobs` skill** before adding one.
160
156
 
161
157
  ## Webhook
162
158
 
163
- Inbound HTTP endpoints that invoke a method directly and synchronously — the caller waits for the method to finish. Use for receiving webhooks from external services (Stripe, GitHub, Shopify, Slack, Twilio). Direct inbound webhooks with signature verification work natively; do **not** build confirmation-token or polling workarounds.
164
-
165
- Routing is by a secret in the URL rather than an auth header, which is what makes it the right fit for provider callbacks — they can't send a bearer token. The API interface is the alternative when the caller can.
166
-
167
- **Load the `webhooks` skill** before adding one — the secret semantics, endpoint URL, input shape, and signature verification are all there.
159
+ Inbound HTTP endpoints that invoke a method directly and synchronously. Use for provider callbacks (Stripe, GitHub, Shopify, Slack, Twilio) — signature verification works natively; do **not** build confirmation-token or polling workarounds. Routing is by a secret in the URL rather than an auth header, which is what fits callers that can't send a bearer token; the API interface is the alternative when they can. **Load the `webhooks` skill** before adding one.
168
160
 
169
161
  ## Email
170
162
 
171
- Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method. Addresses on the app's subdomain are catchall, so per-purpose addresses (`support@`, `receipts@`) work without registering anything.
172
-
173
- **Load the `inboundEmail` skill** before writing the handler — address tiers, `approvedSenders`, the input shape, in-thread replies, and attachments are all there.
163
+ Inbound email triggers: one handler method per app, and the platform routes all inbound mail for the app to it. Addresses on the app's subdomain are catchall, so per-purpose addresses (`support@`, `receipts@`) work without registering anything. **Load the `inboundEmail` skill** before writing the handler.
174
164
 
175
165
  ## MCP (Model Context Protocol)
176
166
 
177
- Expose the app to *external* AI agents — Claude Desktop, Cursor, other people's agents, anything that speaks MCP. Unlike the agent interface (which *is* an agent — its own LLM, personality, and chat UI), MCP has no model of its own; it's the app projected as an MCP server for an outside AI to drive.
178
-
179
- It supports the full MCP surface: tools (methods the agent can call), resources (read-only app data addressable by URI), prompts (parameterized templates), and instructions (server-level guidance for the whole toolset). The platform hosts the server, handles auth, and derives every tool's input schema from the method contract.
180
-
181
- **Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md`. Because the consumer is an external agent with no knowledge of your app, the descriptions are the product — the skill carries both how to write them and the full config contract.
167
+ The app projected as an MCP server for *external* AI agents to drive (Claude Desktop, Cursor, other people's agents). Unlike the agent interface — which IS an agent, with its own LLM — MCP has no model of its own. The platform hosts the server, handles auth, and derives tool schemas from method contracts. **Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md` — the consumer knows nothing about your app, so the descriptions are the product.
182
168
 
183
169
  ## Agent (Conversational Interface)
184
170
 
185
- A conversational interface where an LLM has access to the app's methods as tools. Unlike MCP (which exposes methods for external agents), the agent interface IS the agent — it has its own personality, system prompt, and model config, and orchestrates tool calls against the app's methods internally. Chat runs as the authenticated user, so every tool call carries that user's roles. The config must declare an `auth` block (`{ "requireUser": boolean, "requireRole"?: string[] }`) gating who may chat at all.
186
-
187
- **Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI — the spec frontmatter, compiled output, `agent.json`, and the entire frontend surface are all there.
171
+ A conversational interface where the app's own LLM orchestrates its methods as tools — its own personality, system prompt, and model config (the inverse of MCP). Chat runs as the authenticated user, so every tool call carries that user's roles, and the config must declare an `auth` block (`{ "requireUser": boolean, "requireRole"?: string[] }`) gating who may chat at all. **Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI.
188
172
 
189
173
  ## Voice (Realtime Conversation)
190
174
 
191
- The app's agent as a live voice conversation — the user talks, and the agent answers in sub-second, interruptible speech, calling methods mid-conversation. A sibling of the agent interface, not a mode of it: its own spec, a persona written for the ear rather than the screen, and a smaller toolset where every tool carries a latency class governing how the agent handles the wait out loud. Sessions run as the authenticated user, so tool calls carry that user's roles; the platform handles the realtime media, turn-taking, barge-in, and transcripts. The config must declare an `auth` block (`{ "requireUser": boolean, "requireRole"?: string[] }`) gating who may start a session at all.
192
-
193
- **Load the `voiceInterfaces` skill** before authoring `src/interfaces/voice.md` or building the voice UI — the spoken-register rules, latency classes, spec format, `interface.json`, and the `createVoiceClient()` frontend surface are all there.
175
+ The app's agent as a live, interruptible voice conversation — a sibling of the agent interface, not a mode of it, with its own spec and a smaller toolset where every tool carries a latency class. Sessions run as the authenticated user, and the config must declare the same `auth` block gating who may start a session. **Load the `voiceInterfaces` skill** before authoring `src/interfaces/voice.md` or building the voice UI.
194
176
 
195
177
  ## Manifest Declaration
196
178