create-instant-app 0.22.65 → 0.22.66

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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{Command as xe,Option as u}from"commander";import*as E from"@clack/prompts";import{spawn as he}from"child_process";import{execa as we}from"execa";async function O(){return new Promise(e=>{let t=process.env.SHELL||"/bin/bash",r=he(t,["-i","-c","which claude"],{stdio:["ignore","pipe","ignore"]}),o="";r.stdout.on("data",i=>{o+=i.toString()}),r.on("close",i=>{if(i===0){let n=o.trim();n.includes("aliased to ")?e(n.split("aliased to ")[1]||null):e(n)}else e(null)})})}var B=async(e,t)=>{let r=await O();if(!r)throw new Error("Claude not found in path");await we(r,[e],{stdio:"inherit",cwd:t})};import{version as be}from"@instantdb/version";import G from"path";var W=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e),ye=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,D=e=>{let r=W(e).split("/"),o=r[r.length-1];if(o==="."){let a=G.resolve(process.cwd());o=G.basename(a)}let i=r.findIndex(a=>a.startsWith("@"));r.findIndex(a=>a.startsWith("@"))!==-1&&(o=r.slice(i).join("/"));let n=r.filter(a=>!a.startsWith("@")).join("/");return[o,n]},j=e=>e.trim(),$=e=>{let t=W(e),r=t.split("/"),o=r.findIndex(n=>n.startsWith("@")),i=r[r.length-1];if(r.findIndex(n=>n.startsWith("@"))!==-1&&(i=r.slice(o).join("/")),!(t==="."||ye.test(i??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};import{renderUnwrap as A,UI as c}from"instant-cli/ui";var Te={base:"next-js-app-dir",appName:"awesome-todos",ruleFiles:null,createRepo:!0,prompt:null},V=async()=>{let e=Te,t=new xe().name("Create Instant App").description("A CLI for creating web/mobile applications with InstantDB").argument("[dir]","The name of the application, as well as the name of the directory to create").addOption(new u("-b --base <template>","The base template to scaffold from").choices(["next-js-app-dir","vite-vanilla","expo"])).addOption(new u("-g --git","Create a git repo in the new project").default(!0)).addOption(new u("--expo","Use the Expo starter template").default(!1)).addOption(new u("--next","Use the NextJS starter template").default(!1)).addOption(new u("--vanilla","Use the vanilla JS starter template").default(!1)).addOption(new u("--no-git","Don't create a git repo in the new project")).addOption(new u("--claude","Include a CLAUDE.md file in the scaffold")).addOption(new u("--rules","Include an AGENT.md file in the scaffold")).addOption(new u("--cursor","Include a Cursor rules file in the scaffold")).addOption(new u("--ai","Create a new InstantDB app based off of a prompt. (requires Claude Code)")).version(be).parse(process.argv),r=t.args[0]&&j(t.args[0]);if(r){let n=$(r);if(n)throw new Error("Invalid app name: "+n);e.appName=r}let o=t.opts();if(o.ai&&!await O())throw new Error("--ai only works with Claude Code, but we couldn't find it in your machine. Install it first, and run it again : ). Alternatively you can scaffold out a project without --ai");return await E.group({appName:async()=>{if(r)return r.trim();let n=await A(new c.TextInput({prompt:"What will your project/folder be called?",placeholder:"awesome-todos",defaultValue:"awesome-todos",validate:s=>$(j(s)),modifyOutput:c.ciaModifier()}));return j(n)},prompt:async()=>o.ai?await A(new c.TextInput({prompt:"What would you like to create?",placeholder:"Create an app that...",modifyOutput:c.modifiers.piped([c.ciaModifier()])})):null,base:async({results:n})=>o.base?o.base:o.vanilla?"vite-vanilla":o.next?"next-js-app-dir":o.expo?"expo":n.prompt?A(new c.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Next.js"},{value:"expo",label:"Expo: React Native"}],defaultValue:"next-js-app-dir",modifyOutput:c.modifiers.piped([c.ciaModifier()])})):A(new c.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Next.js"},{value:"vite-vanilla",label:"Vite: Vanilla TS"},{value:"expo",label:"Expo: React Native"}],defaultValue:"next-js-app-dir",modifyOutput:c.modifiers.piped([c.ciaModifier()])})),ruleFiles:async({results:n})=>n.prompt?"claude":n.base!=="next-js-app-dir"&&n.base!=="expo"?null:o.claude?"claude":o.rules?"codex":o.cursor?"cursor":A(new c.Select({promptText:"Which AI tool would you like to add rule files for?",options:[{value:"cursor",label:"Cursor"},{value:"claude",label:"Claude"},{value:"codex",label:"Codex"},{value:"zed",label:"Zed"},{value:"windsurf",label:"Windsurf"},{value:null,label:"None"}],defaultValue:null,modifyOutput:c.ciaModifier()})),createRepo:async()=>o.git!==void 0?o.git:!0},{onCancel(){process.exit(1)}})};import ue from"path";import de from"fs-extra";import{log as me,outro as ot}from"@clack/prompts";import{intro as _e}from"@clack/prompts";import L from"chalk";var ke=` _ _ _
2
+ import{Command as xe,Option as u}from"commander";import*as $ from"@clack/prompts";import{spawn as he}from"child_process";import{execa as we}from"execa";async function O(){return new Promise(e=>{let t=process.env.SHELL||"/bin/bash",r=he(t,["-i","-c","which claude"],{stdio:["ignore","pipe","ignore"]}),o="";r.stdout.on("data",i=>{o+=i.toString()}),r.on("close",i=>{if(i===0){let n=o.trim();n.includes("aliased to ")?e(n.split("aliased to ")[1]||null):e(n)}else e(null)})})}var U=async(e,t)=>{let r=await O();if(!r)throw new Error("Claude not found in path");await we(r,[e],{stdio:"inherit",cwd:t})};import{version as be}from"@instantdb/version";import B from"path";var W=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e),ye=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,D=e=>{let r=W(e).split("/"),o=r[r.length-1];if(o==="."){let a=B.resolve(process.cwd());o=B.basename(a)}let i=r.findIndex(a=>a.startsWith("@"));r.findIndex(a=>a.startsWith("@"))!==-1&&(o=r.slice(i).join("/"));let n=r.filter(a=>!a.startsWith("@")).join("/");return[o,n]},S=e=>e.trim(),E=e=>{let t=W(e),r=t.split("/"),o=r.findIndex(n=>n.startsWith("@")),i=r[r.length-1];if(r.findIndex(n=>n.startsWith("@"))!==-1&&(i=r.slice(o).join("/")),!(t==="."||ye.test(i??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};import{renderUnwrap as A,UI as l}from"instant-cli/ui";var Te={base:"next-js-app-dir",appName:"awesome-todos",ruleFiles:null,createRepo:!0,prompt:null},V=async()=>{let e=Te,t=new xe().name("Create Instant App").description("A CLI for creating web/mobile applications with InstantDB").argument("[dir]","The name of the application, as well as the name of the directory to create").addOption(new u("-b --base <template>","The base template to scaffold from").choices(["next-js-app-dir","vite-vanilla","expo"])).addOption(new u("-g --git","Create a git repo in the new project").default(!0)).addOption(new u("--expo","Use the Expo starter template").default(!1)).addOption(new u("--next","Use the NextJS starter template").default(!1)).addOption(new u("--vanilla","Use the vanilla JS starter template").default(!1)).addOption(new u("--no-git","Don't create a git repo in the new project")).addOption(new u("--claude","Include a CLAUDE.md file in the scaffold")).addOption(new u("--rules","Include an AGENT.md file in the scaffold")).addOption(new u("--cursor","Include a Cursor rules file in the scaffold")).addOption(new u("--ai","Create a new InstantDB app based off of a prompt. (requires Claude Code)")).version(be).parse(process.argv),r=t.args[0]&&S(t.args[0]);if(r){let n=E(r);if(n)throw new Error("Invalid app name: "+n);e.appName=r}let o=t.opts();if(o.ai&&!await O())throw new Error("--ai only works with Claude Code, but we couldn't find it in your machine. Install it first, and run it again : ). Alternatively you can scaffold out a project without --ai");return await $.group({appName:async()=>{if(r)return r.trim();let n=await A(new l.TextInput({prompt:"What will your project/folder be called?",placeholder:"awesome-todos",defaultValue:"awesome-todos",validate:s=>E(S(s)),modifyOutput:l.ciaModifier()}));return S(n)},prompt:async()=>o.ai?await A(new l.TextInput({prompt:"What would you like to create?",placeholder:"Create an app that...",modifyOutput:l.modifiers.piped([l.ciaModifier()])})):null,base:async({results:n})=>o.base?o.base:o.vanilla?"vite-vanilla":o.next?"next-js-app-dir":o.expo?"expo":n.prompt?A(new l.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Next.js"},{value:"expo",label:"Expo: React Native"}],defaultValue:"next-js-app-dir",modifyOutput:l.modifiers.piped([l.ciaModifier()])})):A(new l.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Next.js"},{value:"vite-vanilla",label:"Vite: Vanilla TS"},{value:"expo",label:"Expo: React Native"}],defaultValue:"next-js-app-dir",modifyOutput:l.modifiers.piped([l.ciaModifier()])})),ruleFiles:async({results:n})=>n.prompt?"claude":n.base!=="next-js-app-dir"&&n.base!=="expo"?null:o.claude?"claude":o.rules?"codex":o.cursor?"cursor":A(new l.Select({promptText:"Which AI tool would you like to add rule files for?",options:[{value:"cursor",label:"Cursor"},{value:"claude",label:"Claude"},{value:"codex",label:"Codex"},{value:"zed",label:"Zed"},{value:"windsurf",label:"Windsurf"},{value:null,label:"None"}],defaultValue:null,modifyOutput:l.ciaModifier()})),createRepo:async()=>o.git!==void 0?o.git:!0},{onCancel(){process.exit(1)}})};import ue from"path";import de from"fs-extra";import{log as me,outro as ot}from"@clack/prompts";import{intro as _e}from"@clack/prompts";import L from"chalk";var ke=` _ _ _
3
3
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 (_) | | | |
4
4
  \u2588 \u2588\u2588\u2588\u2588 _ _ __ ___| |_ __ _ _ _ _| |_
5
5
  \u2588 \u2588\u2588\u2588\u2588 | | '_ \\/ __| __/ _\\\`| '_ \\| __|
@@ -7,17 +7,17 @@ import{Command as xe,Option as u}from"commander";import*as E from"@clack/prompts
7
7
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 |_|_| |_|___/\\__\\__,_|_| |_|\\__|`,z=()=>{_e(`
8
8
  `+ke.split(`
9
9
  `).map(e=>`${L.gray("\u2502")}${L.hex("#EA580D").bold(" "+e)}`).join(`
10
- `))};import d from"path";import g from"fs-extra";import H from"path";import{fileURLToPath as Pe}from"url";var ve=Pe(import.meta.url),Ae=H.dirname(ve),f=H.join(Ae,"../");import T from"chalk";var y=()=>{let e=process.env.npm_config_user_agent;return e?e.startsWith("yarn")?"yarn":e.startsWith("pnpm")?"pnpm":e.startsWith("bun")?"bun":"npm":"npm"};import{renderUnwrap as Ie,UI as x}from"instant-cli/ui";import je from"slugify";var J=async(e,t)=>{let r=d.resolve(process.cwd(),t),o=d.join(f,`template/base/${e.base}`);if(g.existsSync(r))if(g.readdirSync(r).length===0)x.log(`${T.cyan.bold(e.appName)} exists but is empty, continuing...`,x.ciaModifier(null));else{let n=await Ie(new x.Select({promptText:T.redBright(`${T.bold("Warning:")} ${T.bold(e.appName)} already exists and isn't empty. How would you like to proceed?`),options:[{label:"Abort installation",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"}],defaultValue:"abort",modifyOutput:x.ciaModifier()}));n==="abort"&&(x.log("Aborting installation..."),process.exit(1)),n==="clear"&&g.emptyDirSync(r)}if(g.copySync(o,r),g.renameSync(d.join(r,"_gitignore"),d.join(r,".gitignore")),g.renameSync(d.join(r,"_env"),d.join(r,".env")),y()==="pnpm"&&e.base==="expo"&&g.appendFile(d.join(r,".npmrc"),`node-linker=hoisted
11
- enable-pre-post-scripts=true`),e.base==="expo"){let n=je.default(t);F(d.join(r,"app.json"),'"name": "expo-template"',`"name": "${t}"`),F(d.join(r,"app.json"),'"slug": "expo-template"',`"slug": "${n}"`),F(d.join(r,"app/_layout.tsx"),'"My Instant App"',`"${t}"`)}let i=e.appName==="."?"App":T.hex("#EA570B").bold(e.appName);return x.log(T.dim(`${i} scaffolded successfully!`),x.ciaModifier(null)),r},F=(e,t,r)=>{let i=g.readFileSync(e,"utf8").replaceAll(t,r);g.writeFileSync(e,i)};import{execa as Ne}from"execa";import{renderUnwrap as Se,UI as N}from"instant-cli/ui";var K=async(e,t)=>{let r=Ne(e,["install"],{cwd:t}),o=await Se(new N.Spinner({promise:r,workingText:`Installing dependencies with ${e}...`,doneText:"Successfully installed dependencies!",modifyOutput:N.ciaModifier(null)}));o.exitCode!==0&&(N.log(o.stderr,N.ciaModifier(null)),process.exit(1))};import it from"chalk";import w from"fs-extra";import p from"path";var Y=({projectDir:e,base:t,ruleFilesToAdd:r})=>{if(r!==null)switch(r){case"claude":w.copyFileSync(p.join(f,`template/rules/${t}/claude.md`),p.join(e,"CLAUDE.md")),w.copyFileSync(p.join(f,`template/rules/${t}/claude-rules.md`),p.join(e,"instant-rules.md"));break;case"cursor":w.ensureDirSync(p.join(e,".cursor/rules")),w.copyFileSync(p.join(f,`template/rules/${t}/cursor-rules.md`),p.join(e,".cursor/rules/instant.mdc"));break;case"windsurf":w.ensureDirSync(p.join(e,".windsurf/rules")),w.copyFileSync(p.join(f,`template/rules/${t}/windsurf-rules.md`),p.join(e,".windsurf/rules/instant.md"));break;case"zed":w.copyFileSync(p.join(f,`template/rules/${t}/other-rules.md`),p.join(e,"AGENT.md"));break;case"codex":w.copyFileSync(p.join(f,`template/rules/${t}/other-rules.md`),p.join(e,"AGENTS.md"))}};import{execSync as R}from"child_process";import M from"path";import*as Z from"@clack/prompts";import q from"chalk";import{execa as k}from"execa";import Q from"fs-extra";import{renderUnwrap as X,UI as _}from"instant-cli/ui";var Ce=e=>{try{return R("git --version",{cwd:e}),!0}catch{return!1}},Oe=e=>Q.existsSync(M.join(e,".git")),$e=async e=>{try{return await k("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},Ee=()=>{let t=R("git --version").toString().trim().split(" ")[2],r=t?.split(".")[0],o=t?.split(".")[1];return{major:Number(r),minor:Number(o)}},Fe=()=>R("git config --global init.defaultBranch || echo main").toString().trim(),ee=async e=>{if(!Ce(e)){Z.log.warn("Git is not installed. Skipping Git initialization.");return}let t=Oe(e),r=await $e(e),o=M.parse(e).name;if(r&&t){if(!await X(new _.Confirmation({promptText:`${q.redBright.bold("Warning:")} Git is already initialized in "${o}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,defaultValue:!1,modifyOutput:_.ciaModifier()})))return;Q.removeSync(M.join(e,".git"))}else if(r&&!t&&!await X(new _.Confirmation({promptText:`${q.redBright.bold("Warning:")} "${o}" is already in a git worktree.
10
+ `))};import d from"path";import f from"fs-extra";import H from"path";import{fileURLToPath as Pe}from"url";var ve=Pe(import.meta.url),Ae=H.dirname(ve),h=H.join(Ae,"../");import T from"chalk";var w=()=>{let e=process.env.npm_config_user_agent;return e?e.startsWith("yarn")?"yarn":e.startsWith("pnpm")?"pnpm":e.startsWith("bun")?"bun":"npm":"npm"};import{renderUnwrap as Ne,UI as y}from"instant-cli/ui";import Se from"slugify";var J=async(e,t)=>{let r=d.resolve(process.cwd(),t),o=d.join(h,`template/base/${e.base}`);if(f.existsSync(r))if(f.readdirSync(r).length===0)y.log(`${T.cyan.bold(e.appName)} exists but is empty, continuing...`,y.ciaModifier(null));else{let n=await Ne(new y.Select({promptText:T.redBright(`${T.bold("Warning:")} ${T.bold(e.appName)} already exists and isn't empty. How would you like to proceed?`),options:[{label:"Abort installation",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"}],defaultValue:"abort",modifyOutput:y.ciaModifier()}));n==="abort"&&(y.log("Aborting installation..."),process.exit(1)),n==="clear"&&f.emptyDirSync(r)}if(f.copySync(o,r),f.renameSync(d.join(r,"_gitignore"),d.join(r,".gitignore")),f.renameSync(d.join(r,"_env"),d.join(r,".env")),w()==="pnpm"&&e.base==="expo"&&f.appendFile(d.join(r,".npmrc"),`node-linker=hoisted
11
+ enable-pre-post-scripts=true`),e.base==="expo"){let n=Se.default(t);F(d.join(r,"app.json"),'"name": "expo-template"',`"name": "${t}"`),F(d.join(r,"app.json"),'"slug": "expo-template"',`"slug": "${n}"`),F(d.join(r,"app/_layout.tsx"),'"My Instant App"',`"${t}"`)}let i=e.appName==="."?"App":T.hex("#EA570B").bold(e.appName);return y.log(T.dim(`${i} scaffolded successfully!`),y.ciaModifier(null)),r},F=(e,t,r)=>{let i=f.readFileSync(e,"utf8").replaceAll(t,r);f.writeFileSync(e,i)};import{execa as Ie}from"execa";import{renderUnwrap as je,UI as I}from"instant-cli/ui";var K=async(e,t)=>{let r=Ie(e,["install"],{cwd:t}),o=await je(new I.Spinner({promise:r,workingText:`Installing dependencies with ${e}...`,doneText:"Successfully installed dependencies!",modifyOutput:I.ciaModifier(null)}));o.exitCode!==0&&(I.log(o.stderr,I.ciaModifier(null)),process.exit(1))};import it from"chalk";import x from"fs-extra";import c from"path";var Y=({projectDir:e,ruleFilesToAdd:t})=>{if(t!==null)switch(t){case"claude":x.copyFileSync(c.join(h,"template/rules/AGENTS.md"),c.join(e,"CLAUDE.md"));break;case"cursor":x.ensureDirSync(c.join(e,".cursor/rules")),x.copyFileSync(c.join(h,"template/rules/cursor-rules.md"),c.join(e,".cursor/rules/instant.mdc"));break;case"windsurf":x.ensureDirSync(c.join(e,".windsurf/rules")),x.copyFileSync(c.join(h,"template/rules/windsurf-rules.md"),c.join(e,".windsurf/rules/instant.md"));break;case"zed":x.copyFileSync(c.join(h,"template/rules/AGENTS.md"),c.join(e,"AGENTS.md"));break;case"codex":x.copyFileSync(c.join(h,"template/rules/AGENTS.md"),c.join(e,"AGENTS.md"));break}};import{execSync as R}from"child_process";import M from"path";import*as Z from"@clack/prompts";import q from"chalk";import{execa as k}from"execa";import Q from"fs-extra";import{renderUnwrap as X,UI as _}from"instant-cli/ui";var Ce=e=>{try{return R("git --version",{cwd:e}),!0}catch{return!1}},Oe=e=>Q.existsSync(M.join(e,".git")),Ee=async e=>{try{return await k("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},$e=()=>{let t=R("git --version").toString().trim().split(" ")[2],r=t?.split(".")[0],o=t?.split(".")[1];return{major:Number(r),minor:Number(o)}},Fe=()=>R("git config --global init.defaultBranch || echo main").toString().trim(),ee=async e=>{if(!Ce(e)){Z.log.warn("Git is not installed. Skipping Git initialization.");return}let t=Oe(e),r=await Ee(e),o=M.parse(e).name;if(r&&t){if(!await X(new _.Confirmation({promptText:`${q.redBright.bold("Warning:")} Git is already initialized in "${o}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,defaultValue:!1,modifyOutput:_.ciaModifier()})))return;Q.removeSync(M.join(e,".git"))}else if(r&&!t&&!await X(new _.Confirmation({promptText:`${q.redBright.bold("Warning:")} "${o}" is already in a git worktree.
12
12
  Would you still like to initialize a new git repository in this directory?
13
- `,defaultValue:!1,modifyOutput:_.ciaModifier()})))return;try{let i=Fe(),{major:n,minor:a}=Ee();n<2||n==2&&a<28?(await k("git",["init"],{cwd:e}),await k("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await k("git",["init",`--initial-branch=${i}`],{cwd:e}),await k("git",["add","."],{cwd:e}),await k("git",["commit","-m","Initial commit (create-instant-app)"],{cwd:e}),_.log("Git repository initialized successfully.",_.ciaModifier(null))}catch{}};import Me from"env-paths";import*as Re from"@clack/prompts";import{mkdir as Ue,readFile as Be,writeFile as Ge}from"node:fs/promises";import We from"open";import{join as De}from"node:path";import{randomUUID as ne}from"node:crypto";var te=!!process.env.INSTANT_CLI_DEV,re=te?"http://localhost:3000":"https://instantdb.com",U=process.env.INSTANT_CLI_API_URI||(te?"http://localhost:8888":"https://api.instantdb.com");async function P({path:e,body:t,method:r="GET",authToken:o}){let n=await fetch(`${U}${e}`,{method:r??"GET",headers:{Authorization:o?`Bearer ${o}`:"","Content-Type":"application/json"},body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(3e5)}),a;try{a=await n.json()}catch{a=null}if(!n.ok){let s=a.message||a.hint?.errors?.[0]?.message||"There was an error";throw new Error(s)}return a}import{renderUnwrap as S,UI as m}from"instant-cli/ui";var Ve=!!process.env.INSTANT_CLI_DEV,Le=!!process.env.INSTANT_CLI_FORCE_EPHEMERAL;function ie(){let e=`instantdb-${Ve?"dev":"prod"}`,{config:t}=Me(e);return{authConfigFilePath:De(t,"a"),appConfigDirPath:t}}var ze=async(e,t,r)=>{let o=ne(),i=ne();return await P({method:"POST",authToken:t,path:"/dash/apps",body:{id:o,title:e,admin_token:i,org_id:r}}),{appID:o,adminToken:i,source:"created"}},He=async e=>await P({method:"GET",path:"/dash",authToken:e}),Je=async(e,t)=>await P({method:"GET",path:`/dash/orgs/${t}`,authToken:e});var Ke=async()=>Le?null:process.env.INSTANT_CLI_AUTH_TOKEN?process.env.INSTANT_CLI_AUTH_TOKEN:await Be(ie().authConfigFilePath,"utf-8").catch(()=>null),oe=async e=>{let t=await P({authToken:null,method:"POST",path:"/dash/apps/ephemeral",body:{title:e,rules:{$users:{view:"true"},$files:{allow:{view:"true",create:"true",delete:"true"}}}}});return{appId:t.app.id,adminToken:t.app["admin-token"]}},ae=async()=>{let e=await Ke();if(!e){let i=await S(new m.Select({promptText:"You are not logged in.",options:[{label:"Create temporary app",value:"ephemeral"},{label:"Login to choose existing app",value:"login"},{label:"Create app later",value:"skip"}],modifyOutput:m.ciaModifier()}));if(i==="login"){let n=await P({authToken:null,method:"POST",path:"/dash/cli/auth/register"}),{secret:a,ticket:s}=n;We(`${re}/dash?ticket=${s}`);let l=qe({secret:a}),h=await S(new m.Spinner({promise:l,workingText:"Waiting for login in browser",disappearWhenDone:!0,modifyOutput:m.ciaModifier(null)}));await Xe(h.token),e=h.token}if(i==="skip")return m.log("Skipping app link step",m.ciaModifier(null)),null;if(i==="ephemeral"){let n=await S(new m.TextInput({defaultValue:"my-cool-app",prompt:"Enter a name for your temporary app:",placeholder:"my-cool-app",modifyOutput:m.ciaModifier()}));return{...await oe(n),approach:"ephemeral"}}}if(!e)return null;let t=await He(e),r=t.orgs.filter(i=>i.role!=="app-member");return t.orgs=r,await S(new m.AppSelector({startingMenuIndex:0,allowCreate:!0,allowEphemeral:!0,api:{getDash(){return t},createEphemeralApp(i){return oe(i)},getAppsForOrg:async i=>{let{apps:n}=await Je(e,i);return{apps:n}},createApp:async(i,n)=>{let{appID:a,adminToken:s}=await ze(i,e,n);return{appId:a,adminToken:s}}},modifyOutput:m.ciaModifier()}))};function Ye(e){return new Promise(t=>setTimeout(t,e))}async function qe({secret:e}){for(let t=1;t<=120;t++){await Ye(1e3);let r=await fetch(`${U}/dash/cli/auth/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:e})});if(!(!r.ok&&(await r.json()).hint.errors?.[0]?.issue==="waiting-for-user")&&r.ok)return r.json()}throw new Error("Timed out waiting for login")}async function Xe(e){let t=ie();return await Ue(t.appConfigDirPath,{recursive:!0}),Ge(t.authConfigFilePath,e,"utf-8")}import Ze from"fs-extra";import Qe from"path";var et={"next-js-app-dir":"NEXT_PUBLIC_INSTANT_APP_ID","vite-vanilla":"VITE_INSTANT_APP_ID",expo:"EXPO_PUBLIC_INSTANT_APP_ID"},se=(e,t,r,o)=>{let i=Qe.join(t,".env"),a=`${et[e.base]}=${r}
14
- INSTANT_APP_ADMIN_TOKEN=${o}`;Ze.writeFileSync(i,a)};import{stdin as b,stdout as pe}from"process";async function tt(){return new Promise(e=>{if(!pe.isTTY||!b.isTTY){e(null);return}let t=b.isRaw;t||b.setRawMode(!0);let r=setTimeout(()=>{i(),e(null)},100),o="",i=()=>{clearTimeout(r),b.removeListener("data",n),!t&&b.isTTY&&b.setRawMode(!1)},n=a=>{let s=a.toString();o+=s;let l=o.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x1b\\|\x07)/);if(l){i();let[,h,fe,ge]=l;e(`rgb:${h}/${fe}/${ge}`)}};b.on("data",n),pe.write("\x1B]11;?\x1B\\")})}function rt(e){let t=e.match(/rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);if(!t)return null;let[,r,o,i]=t,n=Math.round(parseInt(r,16)/257),a=Math.round(parseInt(o,16)/257),s=Math.round(parseInt(i,16)/257);return{r:n,g:a,b:s}}function nt(e){return(.299*e.r+.587*e.g+.114*e.b)/255>.5}async function le(){try{let e=await tt();if(!e)return"unknown";let t=rt(e);return t?nt(t)?"light":"dark":"unknown"}catch(e){return console.error("Error detecting terminal theme:",e),"unknown"}}import I from"chalk";var v=(e,t)=>e==="light"?I.bgYellowBright(t):I.bgBlackBright(t),ce=(e,t=console.log,r=!1)=>{let o=(process.stdout.columns||80)-4,i=e.split(`
15
- `),n=!0;i.forEach(a=>{if(a.length===0){n?(t(""),n=!1):console.log(I.gray("\u2502"));return}for(let s=0;s<a.length;s+=o){let l=a.slice(s,s+o);s+o<a.length&&l.length===o&&(l=l+"-"),n?(t(l),n=!1):console.log(I.gray("\u2502 ")+l)}}),r&&console.log(I.gray("\u2502"))};var C="\x1B[?25h";import{execa as at}from"execa";var st=async()=>{process.argv.some(h=>["-h","--help","--version","-V"].includes(h))||z();let e=await le(),t=await V(),[r,o]=D(t.appName),i=y(),n=await J(t,o);Y({projectDir:n,base:t.base,ruleFilesToAdd:t.ruleFiles});let a=await ae();a&&se(t,n,a.appId,a.adminToken),await K(y(),n);let s=de.readJSONSync(ue.join(n,"package.json"));if(s.name=r,i!=="bun"){let{stdout:h}=await at(i,["-v"],{cwd:n});s.packageManager=`${i}@${h.trim()}`}de.writeJSONSync(ue.join(n,"package.json"),s,{spaces:2}),t.createRepo&&await ee(n),t.prompt&&(await B(t.prompt,n),process.stdout.write(C)),ot("Done!");let l=t.base==="expo"?"start":"dev";a?(console.log(`
13
+ `,defaultValue:!1,modifyOutput:_.ciaModifier()})))return;try{let i=Fe(),{major:n,minor:a}=$e();n<2||n==2&&a<28?(await k("git",["init"],{cwd:e}),await k("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await k("git",["init",`--initial-branch=${i}`],{cwd:e}),await k("git",["add","."],{cwd:e}),await k("git",["commit","-m","Initial commit (create-instant-app)"],{cwd:e}),_.log("Git repository initialized successfully.",_.ciaModifier(null))}catch{}};import Me from"env-paths";import*as Re from"@clack/prompts";import{mkdir as Ge,readFile as Ue,writeFile as Be}from"node:fs/promises";import We from"open";import{join as De}from"node:path";import{randomUUID as ne}from"node:crypto";var te=!!process.env.INSTANT_CLI_DEV,re=te?"http://localhost:3000":"https://instantdb.com",G=process.env.INSTANT_CLI_API_URI||(te?"http://localhost:8888":"https://api.instantdb.com");async function P({path:e,body:t,method:r="GET",authToken:o}){let n=await fetch(`${G}${e}`,{method:r??"GET",headers:{Authorization:o?`Bearer ${o}`:"","Content-Type":"application/json"},body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(3e5)}),a;try{a=await n.json()}catch{a=null}if(!n.ok){let s=a.message||a.hint?.errors?.[0]?.message||"There was an error";throw new Error(s)}return a}import{renderUnwrap as j,UI as m}from"instant-cli/ui";var Ve=!!process.env.INSTANT_CLI_DEV,Le=!!process.env.INSTANT_CLI_FORCE_EPHEMERAL;function ie(){let e=`instantdb-${Ve?"dev":"prod"}`,{config:t}=Me(e);return{authConfigFilePath:De(t,"a"),appConfigDirPath:t}}var ze=async(e,t,r)=>{let o=ne(),i=ne();return await P({method:"POST",authToken:t,path:"/dash/apps",body:{id:o,title:e,admin_token:i,org_id:r}}),{appID:o,adminToken:i,source:"created"}},He=async e=>await P({method:"GET",path:"/dash",authToken:e}),Je=async(e,t)=>await P({method:"GET",path:`/dash/orgs/${t}`,authToken:e});var Ke=async()=>Le?null:process.env.INSTANT_CLI_AUTH_TOKEN?process.env.INSTANT_CLI_AUTH_TOKEN:await Ue(ie().authConfigFilePath,"utf-8").catch(()=>null),oe=async e=>{let t=await P({authToken:null,method:"POST",path:"/dash/apps/ephemeral",body:{title:e,rules:{$users:{view:"true"},$files:{allow:{view:"true",create:"true",delete:"true"}}}}});return{appId:t.app.id,adminToken:t.app["admin-token"]}},ae=async()=>{let e=await Ke();if(!e){let i=await j(new m.Select({promptText:"You are not logged in.",options:[{label:"Create temporary app",value:"ephemeral"},{label:"Login to choose existing app",value:"login"},{label:"Create app later",value:"skip"}],modifyOutput:m.ciaModifier()}));if(i==="login"){let n=await P({authToken:null,method:"POST",path:"/dash/cli/auth/register"}),{secret:a,ticket:s}=n;We(`${re}/dash?ticket=${s}`);let p=qe({secret:a}),g=await j(new m.Spinner({promise:p,workingText:"Waiting for login in browser",disappearWhenDone:!0,modifyOutput:m.ciaModifier(null)}));await Xe(g.token),e=g.token}if(i==="skip")return m.log("Skipping app link step",m.ciaModifier(null)),null;if(i==="ephemeral"){let n=await j(new m.TextInput({defaultValue:"my-cool-app",prompt:"Enter a name for your temporary app:",placeholder:"my-cool-app",modifyOutput:m.ciaModifier()}));return{...await oe(n),approach:"ephemeral"}}}if(!e)return null;let t=await He(e),r=t.orgs.filter(i=>i.role!=="app-member");return t.orgs=r,await j(new m.AppSelector({startingMenuIndex:0,allowCreate:!0,allowEphemeral:!0,api:{getDash(){return t},createEphemeralApp(i){return oe(i)},getAppsForOrg:async i=>{let{apps:n}=await Je(e,i);return{apps:n}},createApp:async(i,n)=>{let{appID:a,adminToken:s}=await ze(i,e,n);return{appId:a,adminToken:s}}},modifyOutput:m.ciaModifier()}))};function Ye(e){return new Promise(t=>setTimeout(t,e))}async function qe({secret:e}){for(let t=1;t<=120;t++){await Ye(1e3);let r=await fetch(`${G}/dash/cli/auth/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:e})});if(!(!r.ok&&(await r.json()).hint.errors?.[0]?.issue==="waiting-for-user")&&r.ok)return r.json()}throw new Error("Timed out waiting for login")}async function Xe(e){let t=ie();return await Ge(t.appConfigDirPath,{recursive:!0}),Be(t.authConfigFilePath,e,"utf-8")}import Ze from"fs-extra";import Qe from"path";var et={"next-js-app-dir":"NEXT_PUBLIC_INSTANT_APP_ID","vite-vanilla":"VITE_INSTANT_APP_ID",expo:"EXPO_PUBLIC_INSTANT_APP_ID"},se=(e,t,r,o)=>{let i=Qe.join(t,".env"),a=`${et[e.base]}=${r}
14
+ INSTANT_APP_ADMIN_TOKEN=${o}`;Ze.writeFileSync(i,a)};import{stdin as b,stdout as pe}from"process";async function tt(){return new Promise(e=>{if(!pe.isTTY||!b.isTTY){e(null);return}let t=b.isRaw;t||b.setRawMode(!0);let r=setTimeout(()=>{i(),e(null)},100),o="",i=()=>{clearTimeout(r),b.removeListener("data",n),!t&&b.isTTY&&b.setRawMode(!1)},n=a=>{let s=a.toString();o+=s;let p=o.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x1b\\|\x07)/);if(p){i();let[,g,fe,ge]=p;e(`rgb:${g}/${fe}/${ge}`)}};b.on("data",n),pe.write("\x1B]11;?\x1B\\")})}function rt(e){let t=e.match(/rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);if(!t)return null;let[,r,o,i]=t,n=Math.round(parseInt(r,16)/257),a=Math.round(parseInt(o,16)/257),s=Math.round(parseInt(i,16)/257);return{r:n,g:a,b:s}}function nt(e){return(.299*e.r+.587*e.g+.114*e.b)/255>.5}async function le(){try{let e=await tt();if(!e)return"unknown";let t=rt(e);return t?nt(t)?"light":"dark":"unknown"}catch(e){return console.error("Error detecting terminal theme:",e),"unknown"}}import N from"chalk";var v=(e,t)=>e==="light"?N.bgYellowBright(t):N.bgBlackBright(t),ce=(e,t=console.log,r=!1)=>{let o=(process.stdout.columns||80)-4,i=e.split(`
15
+ `),n=!0;i.forEach(a=>{if(a.length===0){n?(t(""),n=!1):console.log(N.gray("\u2502"));return}for(let s=0;s<a.length;s+=o){let p=a.slice(s,s+o);s+o<a.length&&p.length===o&&(p=p+"-"),n?(t(p),n=!1):console.log(N.gray("\u2502 ")+p)}}),r&&console.log(N.gray("\u2502"))};var C="\x1B[?25h";import{execa as at}from"execa";var st=async()=>{process.argv.some(g=>["-h","--help","--version","-V"].includes(g))||z();let e=await le(),t=await V(),[r,o]=D(t.appName),i=w(),n=await J(t,o);Y({projectDir:n,ruleFilesToAdd:t.ruleFiles});let a=await ae();a&&se(t,n,a.appId,a.adminToken),await K(w(),n);let s=de.readJSONSync(ue.join(n,"package.json"));if(s.name=r,i!=="bun"){let{stdout:g}=await at(i,["-v"],{cwd:n});s.packageManager=`${i}@${g.trim()}`}de.writeJSONSync(ue.join(n,"package.json"),s,{spaces:2}),t.createRepo&&await ee(n),t.prompt&&(await U(t.prompt,n),process.stdout.write(C)),ot("Done!");let p=t.base==="expo"?"start":"dev";a?(console.log(`
16
16
  \u{1F389} Success! Your project is ready to go!
17
17
 
18
18
  To get started:
19
19
  1. ${v(e,"cd "+o)}
20
- 2. ${v(e,y()+" run "+l)}
20
+ 2. ${v(e,w()+" run "+p)}
21
21
  `),a.approach==="ephemeral"&&console.log(`
22
22
  An ephemeral app has been created and added to your .env file.
23
23
  It will expire in two weeks. For a permanent app, sign in and use ${v(e,"npx instant-cli claim")}
@@ -28,5 +28,5 @@ INSTANT_APP_ADMIN_TOKEN=${o}`;Ze.writeFileSync(i,a)};import{stdin as b,stdout as
28
28
  1. ${v(e,"cd "+o)}
29
29
  2. Create a new app on ${it.underline("www.instantdb.com")}
30
30
  3. Add your APP_ID to the .env file
31
- 4. ${v(e,y()+" run "+l)}
31
+ 4. ${v(e,w()+" run "+p)}
32
32
  `)};st().catch(e=>{me.error("Aborting installation..."),ce(e.message,me.error),process.stdout.write(C),process.exit(1)});process.on("SIGINT",()=>{process.stdout.write(C),process.exit(0)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-instant-app",
3
- "version": "0.22.65",
3
+ "version": "0.22.66",
4
4
  "description": "Scaffold a new web/mobile app with InstantDB",
5
5
  "homepage": "https://github.com/instantdb/instant/tree/main/client/packages/create-instant-app",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "ora": "6.3.1",
34
34
  "slugify": "^1.6.6",
35
35
  "sort-package-json": "^2.10.0",
36
- "@instantdb/version": "0.22.65",
37
- "instant-cli": "0.22.65"
36
+ "@instantdb/version": "0.22.66",
37
+ "instant-cli": "0.22.66"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@anthropic-ai/sdk": "^0.60.0",
@@ -0,0 +1,346 @@
1
+ Act as a world-class senior frontend engineer with deep expertise in InstantDB
2
+ and UI/UX design. Your primary goal is to generate complete and functional apps
3
+ with excellent visual asthetics using InstantDB as the backend.
4
+
5
+ # About InstantDB aka Instant
6
+
7
+ Instant is a client-side database (Modern Firebase) with built-in queries, transactions, auth, permissions, storage, real-time, and offline support.
8
+
9
+ # Instant SDKs
10
+
11
+ Instant provides client-side JS SDKs and an admin SDK:
12
+
13
+ - `@instantdb/core` --- vanilla JS
14
+ - `@instantdb/react` --- React
15
+ - `@instantdb/react-native` --- React Native / Expo
16
+ - `@instantdb/admin` --- backend scripts / servers
17
+
18
+ When installing, always check what package manager the project uses (npm, pnpm,
19
+ bun) first and then install the latest version of the Instant SDK.
20
+
21
+ # Managing Instant Apps
22
+
23
+ ## Prerequisites
24
+
25
+ Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
26
+ Look for an app id and admin token in `.env` or another env file.
27
+
28
+ If schema/perm files exist but the app id/admin token are missing, ask the user where to find them or whether to create a new app.
29
+
30
+ To create a new app:
31
+
32
+ ```bash
33
+ npx instant-cli init-without-files --title <APP_NAME>
34
+ ```
35
+
36
+ This outputs an app id and admin token. Store them in an env file.
37
+
38
+ If you have an app id/admin token but no schema/perm files, pull them:
39
+
40
+ ```bash
41
+ npx instant-cli pull --app <APP_ID> --token <ADMIN_TOKEN> --yes
42
+ ```
43
+
44
+ ## Schema changes
45
+
46
+ Edit `instant.schema.ts`, then push:
47
+
48
+ ```bash
49
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --yes
50
+ ```
51
+
52
+ New fields = additions; missing fields = deletions.
53
+
54
+ To rename fields:
55
+
56
+ ```bash
57
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
58
+ ```
59
+
60
+ ## Permission changes
61
+
62
+ Edit `instant.perms.ts`, then push:
63
+
64
+ ```bash
65
+ npx instant-cli push perms --app <APP_ID> --token <ADMIN_TOKEN> --yes
66
+ ```
67
+
68
+ # CRITICAL Query Guidelines
69
+
70
+ CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
71
+
72
+ CRITICAL: You MUST index any field you want to filter or order by in the schema. If you do not, you will get an error when you try to filter or order by it.
73
+
74
+ Here is how ordering works:
75
+
76
+ ```
77
+ Ordering: order: { field: 'asc' | 'desc' }
78
+
79
+ Example: $: { order: { dueDate: 'asc' } }
80
+
81
+ Notes: - Field must be indexed + typed in schema
82
+ - Cannot order by nested attributes (e.g. 'owner.name')
83
+ ```
84
+
85
+ CRITICAL: Here is a concise summary of the `where` operator map which defines all the filtering options you can use with InstantDB queries to narrow results based on field values, comparisons, arrays, text patterns, and logical conditions.
86
+
87
+ ```
88
+ Equality: { field: value }
89
+
90
+ Inequality: { field: { $ne: value } }
91
+
92
+ Null checks: { field: { $isNull: true | false } }
93
+
94
+ Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
95
+
96
+ Sets: { field: { $in: [v1, v2] } }
97
+
98
+ Substring: { field: { $like: 'Get%' } } // case-sensitive
99
+ { field: { $ilike: '%get%' } } // case-insensitive
100
+
101
+ Logic: and: [ {...}, {...} ]
102
+ or: [ {...}, {...} ]
103
+
104
+ Nested fields: 'relation.field': value
105
+ ```
106
+
107
+ CRITICAL: The operator map above is the full set of `where` filters Instant
108
+ supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
109
+ `$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
110
+
111
+ CRITICAL: Pagination keys (`limit`, `offset`, `first`, `after`, `last`, `before`) only work on top-level namespaces. DO NOT use them on nested relations or else you will get an error.
112
+
113
+ CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
114
+
115
+ # CRITICAL Permission Guidelines
116
+
117
+ Below are some CRITICAL guidelines for writing permissions in InstantDB.
118
+
119
+ ## data.ref
120
+
121
+ - Use `data.ref("<path.to.attr>")` for linked attributes.
122
+ - Always returns a **list**.
123
+ - Must end with an **attribute**.
124
+
125
+ **Correct**
126
+
127
+ ```cel
128
+ auth.id in data.ref('post.author.id') // auth.id in list of author ids
129
+ data.ref('owner.id') == [] // there is no owner
130
+ ```
131
+
132
+ **Errors**
133
+
134
+ ```cel
135
+ auth.id in data.post.author.id
136
+ auth.id in data.ref('author')
137
+ data.ref('admins.id') == auth.id
138
+ auth.id == data.ref('owner.id')
139
+ data.ref('owner.id') == null
140
+ data.ref('owner.id').length > 0
141
+ ```
142
+
143
+ ## auth.ref
144
+
145
+ - Same as `data.ref` but path must start with `$user`.
146
+ - Returns a list.
147
+
148
+ **Correct**
149
+
150
+ ```cel
151
+ 'admin' in auth.ref('$user.role.type')
152
+ auth.ref('$user.role.type')[0] == 'admin'
153
+ ```
154
+
155
+ **Errors**
156
+
157
+ ```cel
158
+ auth.ref('role.type')
159
+ auth.ref('$user.role.type') == 'admin'
160
+ ```
161
+
162
+ ## Unsupported
163
+
164
+ ```cel
165
+ newData.ref('x')
166
+ data.ref(someVar + '.members.id')
167
+ ```
168
+
169
+ # Best Practices
170
+
171
+ ## Pass `schema` when initializing Instant
172
+
173
+ Always pass `schema` when initializing Instant to get type safety for queries and transactions
174
+
175
+ ```tsx
176
+ import schema from '@/instant.schema`
177
+
178
+ // On client
179
+ import { init } from '@instantdb/react'; // or your relevant Instant SDK
180
+ const clientDb = init({ appId, schema });
181
+
182
+ // On backend
183
+ import { init } from '@instantdb/admin';
184
+ const adminDb = init({ appId, adminToken, schema });
185
+ ```
186
+
187
+ ## Use `id()` to generate ids
188
+
189
+ Always use `id()` to generate ids for new entities
190
+
191
+ ```tsx
192
+ import { id } from '@instantdb/react'; // or your relevant Instant SDK
193
+ import { clientDb } from '@/lib/clientDb
194
+ clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
195
+ ```
196
+
197
+ ## Use Instant utility types for data models
198
+
199
+ Always use Instant utility types to type data models
200
+
201
+ ```tsx
202
+ import { AppSchema } from '@/instant.schema';
203
+
204
+ type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
205
+ type PostsWithProfile = InstaQLEntity<
206
+ AppSchema,
207
+ 'posts',
208
+ { author: { avatar: {} } }
209
+ >; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
210
+ ```
211
+
212
+ ## Use `db.useAuth` or `db.subscribeAuth` for auth state
213
+
214
+ ```tsx
215
+ import { clientDb } from '@/lib/clientDb';
216
+
217
+ // For react/react-native apps use db.useAuth
218
+ function App() {
219
+ const { isLoading, user, error } = clientDb.useAuth();
220
+ if (isLoading) { return null; }
221
+ if (error) { return <Error message={error.message /}></div>; }
222
+ if (user) { return <Main />; }
223
+ return <Login />;
224
+ }
225
+
226
+ // For vanilla JS apps use db.subscribeAuth
227
+ function App() {
228
+ renderLoading();
229
+ db.subscribeAuth((auth) => {
230
+ if (auth.error) { renderAuthError(auth.error.message); }
231
+ else if (auth.user) { renderLoggedInPage(auth.user); }
232
+ else { renderSignInPage(); }
233
+ });
234
+ }
235
+ ```
236
+
237
+ # Ad-hoc queries & transactions
238
+
239
+ Use `@instantdb/admin` to run ad-hoc queries and transactions on the backend.
240
+ Here is an example schema for a chat app along with seed and reset scripts.
241
+
242
+ ```tsx
243
+ // instant.schema.ts
244
+ const _schema = i.schema({
245
+ entities: {
246
+ $users: i.entity({
247
+ email: i.string().unique().indexed().optional(),
248
+ }),
249
+ profiles: i.entity({
250
+ displayName: i.string(),
251
+ }),
252
+ channels: i.entity({
253
+ name: i.string().indexed(),
254
+ }),
255
+ messages: i.entity({
256
+ content: i.string(),
257
+ timestamp: i.number().indexed(),
258
+ }),
259
+ },
260
+ links: {
261
+ userProfile: {
262
+ forward: { on: "profiles", has: "one", label: "user", onDelete: "cascade" }, // IMPORTANT: `cascade` can only be used in a has-one link
263
+ reverse: { on: "$users", has: "one", label: "profile" },
264
+ },
265
+ authorMessages: {
266
+ forward: { on: "messages", has: "one", label: "author", onDelete: "cascade" },
267
+ reverse: { on: "profiles", has: "many", label: "messages", },
268
+ },
269
+ channelMessages: {
270
+ forward: { on: "messages", has: "one", label: "channel", onDelete: "cascade" },
271
+ reverse: { on: "channels", has: "many", label: "messages" },
272
+ },
273
+ },
274
+ });
275
+
276
+ // scripts/seed.ts
277
+ import { id } from "@instantdb/admin";
278
+ import { adminDb } from "@/lib/adminDb";
279
+
280
+ const users: Record<string, User> = { ... }
281
+ const channels: Record<string, Channel> = { ... }
282
+ const mockMessages: Message[] = [ ... ]
283
+
284
+ function seed() {
285
+ console.log("Seeding db...");
286
+ const userTxs = Object.values(users).map(u => adminDb.tx.$users[u.id].create({}));
287
+ const profileTxs = Object.values(users).map(u => adminDb.tx.profiles[u.id].create({ displayName: u.displayName }).link({ user: u.id }));
288
+ const channelTxs = Object.values(channels).map(c => adminDb.tx.channels[c.id].create({ name: c.name }))
289
+ const messageTxs = mockMessages.map(m => {
290
+ const messageId = id();
291
+ return adminDb.tx.messages[messageId].create({
292
+ content: m.content,
293
+ timestamp: m.timestamp,
294
+ })
295
+ .link({ author: users[m.author].id })
296
+ .link({ channel: channels[m.channel].id });
297
+ })
298
+
299
+ adminDb.transact([...userTxs, ...profileTxs, ...channelTxs, ...messageTxs]);
300
+ }
301
+
302
+ seed();
303
+
304
+ // scripts/reset.ts
305
+ import { adminDb } from "@/lib/adminDb";
306
+
307
+ async function reset() {
308
+ console.log("Resetting database...");
309
+ const { $users, channels } = await adminDb.query({ $users: {}, channels: {} });
310
+
311
+ // Deleting all users will cascade delete profiles and messages
312
+ const userTxs = $users.map(user => adminDb.tx.$users[user.id].delete());
313
+
314
+ const channelTxs = channels.map(channel => adminDb.tx.channels[channel.id].delete());
315
+ adminDb.transact([...userTxs, ...channelTxs]);
316
+ }
317
+
318
+ reset();
319
+ ```
320
+
321
+ # Instant Documentation
322
+
323
+ The bullets below are links to the Instant documentation. They provide detailed information on how to use different features of InstantDB. Each line follows the pattern of
324
+
325
+ - [TOPIC](URL): Description of the topic.
326
+
327
+ Fetch the URL for a topic to learn more about it.
328
+
329
+ - [Common mistakes](https://instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
330
+ - [Initializing Instant](https://instantdb.com/docs/init.md): How to integrate Instant with your app.
331
+ - [Modeling data](https://instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
332
+ - [Writing data](https://instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
333
+ - [Reading data](https://instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
334
+ - [Instant on the Backend](https://instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
335
+ - [Patterns](https://instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
336
+ - [Auth](https://instantdb.com/docs/auth.md): Instant supports magic code, OAuth, Clerk, and custom auth.
337
+ - [Auth](https://instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
338
+ - [Managing users](https://instantdb.com/docs/users.md): How to manage users in your Instant app.
339
+ - [Presence, Cursors, and Activity](https://instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
340
+ - [Instant CLI](https://instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
341
+ - [Storage](https://instantdb.com/docs/storage.md): How to upload and serve files with Instant.
342
+
343
+ # Final Note
344
+
345
+ Think before you answer. Make sure your code passes typechecks.
346
+ Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!
@@ -0,0 +1,352 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ Act as a world-class senior frontend engineer with deep expertise in InstantDB
8
+ and UI/UX design. Your primary goal is to generate complete and functional apps
9
+ with excellent visual asthetics using InstantDB as the backend.
10
+
11
+ # About InstantDB aka Instant
12
+
13
+ Instant is a client-side database (Modern Firebase) with built-in queries, transactions, auth, permissions, storage, real-time, and offline support.
14
+
15
+ # Instant SDKs
16
+
17
+ Instant provides client-side JS SDKs and an admin SDK:
18
+
19
+ - `@instantdb/core` --- vanilla JS
20
+ - `@instantdb/react` --- React
21
+ - `@instantdb/react-native` --- React Native / Expo
22
+ - `@instantdb/admin` --- backend scripts / servers
23
+
24
+ When installing, always check what package manager the project uses (npm, pnpm,
25
+ bun) first and then install the latest version of the Instant SDK.
26
+
27
+ # Managing Instant Apps
28
+
29
+ ## Prerequisites
30
+
31
+ Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
32
+ Look for an app id and admin token in `.env` or another env file.
33
+
34
+ If schema/perm files exist but the app id/admin token are missing, ask the user where to find them or whether to create a new app.
35
+
36
+ To create a new app:
37
+
38
+ ```bash
39
+ npx instant-cli init-without-files --title <APP_NAME>
40
+ ```
41
+
42
+ This outputs an app id and admin token. Store them in an env file.
43
+
44
+ If you have an app id/admin token but no schema/perm files, pull them:
45
+
46
+ ```bash
47
+ npx instant-cli pull --app <APP_ID> --token <ADMIN_TOKEN> --yes
48
+ ```
49
+
50
+ ## Schema changes
51
+
52
+ Edit `instant.schema.ts`, then push:
53
+
54
+ ```bash
55
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --yes
56
+ ```
57
+
58
+ New fields = additions; missing fields = deletions.
59
+
60
+ To rename fields:
61
+
62
+ ```bash
63
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
64
+ ```
65
+
66
+ ## Permission changes
67
+
68
+ Edit `instant.perms.ts`, then push:
69
+
70
+ ```bash
71
+ npx instant-cli push perms --app <APP_ID> --token <ADMIN_TOKEN> --yes
72
+ ```
73
+
74
+ # CRITICAL Query Guidelines
75
+
76
+ CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
77
+
78
+ CRITICAL: You MUST index any field you want to filter or order by in the schema. If you do not, you will get an error when you try to filter or order by it.
79
+
80
+ Here is how ordering works:
81
+
82
+ ```
83
+ Ordering: order: { field: 'asc' | 'desc' }
84
+
85
+ Example: $: { order: { dueDate: 'asc' } }
86
+
87
+ Notes: - Field must be indexed + typed in schema
88
+ - Cannot order by nested attributes (e.g. 'owner.name')
89
+ ```
90
+
91
+ CRITICAL: Here is a concise summary of the `where` operator map which defines all the filtering options you can use with InstantDB queries to narrow results based on field values, comparisons, arrays, text patterns, and logical conditions.
92
+
93
+ ```
94
+ Equality: { field: value }
95
+
96
+ Inequality: { field: { $ne: value } }
97
+
98
+ Null checks: { field: { $isNull: true | false } }
99
+
100
+ Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
101
+
102
+ Sets: { field: { $in: [v1, v2] } }
103
+
104
+ Substring: { field: { $like: 'Get%' } } // case-sensitive
105
+ { field: { $ilike: '%get%' } } // case-insensitive
106
+
107
+ Logic: and: [ {...}, {...} ]
108
+ or: [ {...}, {...} ]
109
+
110
+ Nested fields: 'relation.field': value
111
+ ```
112
+
113
+ CRITICAL: The operator map above is the full set of `where` filters Instant
114
+ supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
115
+ `$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
116
+
117
+ CRITICAL: Pagination keys (`limit`, `offset`, `first`, `after`, `last`, `before`) only work on top-level namespaces. DO NOT use them on nested relations or else you will get an error.
118
+
119
+ CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
120
+
121
+ # CRITICAL Permission Guidelines
122
+
123
+ Below are some CRITICAL guidelines for writing permissions in InstantDB.
124
+
125
+ ## data.ref
126
+
127
+ - Use `data.ref("<path.to.attr>")` for linked attributes.
128
+ - Always returns a **list**.
129
+ - Must end with an **attribute**.
130
+
131
+ **Correct**
132
+
133
+ ```cel
134
+ auth.id in data.ref('post.author.id') // auth.id in list of author ids
135
+ data.ref('owner.id') == [] // there is no owner
136
+ ```
137
+
138
+ **Errors**
139
+
140
+ ```cel
141
+ auth.id in data.post.author.id
142
+ auth.id in data.ref('author')
143
+ data.ref('admins.id') == auth.id
144
+ auth.id == data.ref('owner.id')
145
+ data.ref('owner.id') == null
146
+ data.ref('owner.id').length > 0
147
+ ```
148
+
149
+ ## auth.ref
150
+
151
+ - Same as `data.ref` but path must start with `$user`.
152
+ - Returns a list.
153
+
154
+ **Correct**
155
+
156
+ ```cel
157
+ 'admin' in auth.ref('$user.role.type')
158
+ auth.ref('$user.role.type')[0] == 'admin'
159
+ ```
160
+
161
+ **Errors**
162
+
163
+ ```cel
164
+ auth.ref('role.type')
165
+ auth.ref('$user.role.type') == 'admin'
166
+ ```
167
+
168
+ ## Unsupported
169
+
170
+ ```cel
171
+ newData.ref('x')
172
+ data.ref(someVar + '.members.id')
173
+ ```
174
+
175
+ # Best Practices
176
+
177
+ ## Pass `schema` when initializing Instant
178
+
179
+ Always pass `schema` when initializing Instant to get type safety for queries and transactions
180
+
181
+ ```tsx
182
+ import schema from '@/instant.schema`
183
+
184
+ // On client
185
+ import { init } from '@instantdb/react'; // or your relevant Instant SDK
186
+ const clientDb = init({ appId, schema });
187
+
188
+ // On backend
189
+ import { init } from '@instantdb/admin';
190
+ const adminDb = init({ appId, adminToken, schema });
191
+ ```
192
+
193
+ ## Use `id()` to generate ids
194
+
195
+ Always use `id()` to generate ids for new entities
196
+
197
+ ```tsx
198
+ import { id } from '@instantdb/react'; // or your relevant Instant SDK
199
+ import { clientDb } from '@/lib/clientDb
200
+ clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
201
+ ```
202
+
203
+ ## Use Instant utility types for data models
204
+
205
+ Always use Instant utility types to type data models
206
+
207
+ ```tsx
208
+ import { AppSchema } from '@/instant.schema';
209
+
210
+ type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
211
+ type PostsWithProfile = InstaQLEntity<
212
+ AppSchema,
213
+ 'posts',
214
+ { author: { avatar: {} } }
215
+ >; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
216
+ ```
217
+
218
+ ## Use `db.useAuth` or `db.subscribeAuth` for auth state
219
+
220
+ ```tsx
221
+ import { clientDb } from '@/lib/clientDb';
222
+
223
+ // For react/react-native apps use db.useAuth
224
+ function App() {
225
+ const { isLoading, user, error } = clientDb.useAuth();
226
+ if (isLoading) { return null; }
227
+ if (error) { return <Error message={error.message /}></div>; }
228
+ if (user) { return <Main />; }
229
+ return <Login />;
230
+ }
231
+
232
+ // For vanilla JS apps use db.subscribeAuth
233
+ function App() {
234
+ renderLoading();
235
+ db.subscribeAuth((auth) => {
236
+ if (auth.error) { renderAuthError(auth.error.message); }
237
+ else if (auth.user) { renderLoggedInPage(auth.user); }
238
+ else { renderSignInPage(); }
239
+ });
240
+ }
241
+ ```
242
+
243
+ # Ad-hoc queries & transactions
244
+
245
+ Use `@instantdb/admin` to run ad-hoc queries and transactions on the backend.
246
+ Here is an example schema for a chat app along with seed and reset scripts.
247
+
248
+ ```tsx
249
+ // instant.schema.ts
250
+ const _schema = i.schema({
251
+ entities: {
252
+ $users: i.entity({
253
+ email: i.string().unique().indexed().optional(),
254
+ }),
255
+ profiles: i.entity({
256
+ displayName: i.string(),
257
+ }),
258
+ channels: i.entity({
259
+ name: i.string().indexed(),
260
+ }),
261
+ messages: i.entity({
262
+ content: i.string(),
263
+ timestamp: i.number().indexed(),
264
+ }),
265
+ },
266
+ links: {
267
+ userProfile: {
268
+ forward: { on: "profiles", has: "one", label: "user", onDelete: "cascade" }, // IMPORTANT: `cascade` can only be used in a has-one link
269
+ reverse: { on: "$users", has: "one", label: "profile" },
270
+ },
271
+ authorMessages: {
272
+ forward: { on: "messages", has: "one", label: "author", onDelete: "cascade" },
273
+ reverse: { on: "profiles", has: "many", label: "messages", },
274
+ },
275
+ channelMessages: {
276
+ forward: { on: "messages", has: "one", label: "channel", onDelete: "cascade" },
277
+ reverse: { on: "channels", has: "many", label: "messages" },
278
+ },
279
+ },
280
+ });
281
+
282
+ // scripts/seed.ts
283
+ import { id } from "@instantdb/admin";
284
+ import { adminDb } from "@/lib/adminDb";
285
+
286
+ const users: Record<string, User> = { ... }
287
+ const channels: Record<string, Channel> = { ... }
288
+ const mockMessages: Message[] = [ ... ]
289
+
290
+ function seed() {
291
+ console.log("Seeding db...");
292
+ const userTxs = Object.values(users).map(u => adminDb.tx.$users[u.id].create({}));
293
+ const profileTxs = Object.values(users).map(u => adminDb.tx.profiles[u.id].create({ displayName: u.displayName }).link({ user: u.id }));
294
+ const channelTxs = Object.values(channels).map(c => adminDb.tx.channels[c.id].create({ name: c.name }))
295
+ const messageTxs = mockMessages.map(m => {
296
+ const messageId = id();
297
+ return adminDb.tx.messages[messageId].create({
298
+ content: m.content,
299
+ timestamp: m.timestamp,
300
+ })
301
+ .link({ author: users[m.author].id })
302
+ .link({ channel: channels[m.channel].id });
303
+ })
304
+
305
+ adminDb.transact([...userTxs, ...profileTxs, ...channelTxs, ...messageTxs]);
306
+ }
307
+
308
+ seed();
309
+
310
+ // scripts/reset.ts
311
+ import { adminDb } from "@/lib/adminDb";
312
+
313
+ async function reset() {
314
+ console.log("Resetting database...");
315
+ const { $users, channels } = await adminDb.query({ $users: {}, channels: {} });
316
+
317
+ // Deleting all users will cascade delete profiles and messages
318
+ const userTxs = $users.map(user => adminDb.tx.$users[user.id].delete());
319
+
320
+ const channelTxs = channels.map(channel => adminDb.tx.channels[channel.id].delete());
321
+ adminDb.transact([...userTxs, ...channelTxs]);
322
+ }
323
+
324
+ reset();
325
+ ```
326
+
327
+ # Instant Documentation
328
+
329
+ The bullets below are links to the Instant documentation. They provide detailed information on how to use different features of InstantDB. Each line follows the pattern of
330
+
331
+ - [TOPIC](URL): Description of the topic.
332
+
333
+ Fetch the URL for a topic to learn more about it.
334
+
335
+ - [Common mistakes](https://instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
336
+ - [Initializing Instant](https://instantdb.com/docs/init.md): How to integrate Instant with your app.
337
+ - [Modeling data](https://instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
338
+ - [Writing data](https://instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
339
+ - [Reading data](https://instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
340
+ - [Instant on the Backend](https://instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
341
+ - [Patterns](https://instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
342
+ - [Auth](https://instantdb.com/docs/auth.md): Instant supports magic code, OAuth, Clerk, and custom auth.
343
+ - [Auth](https://instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
344
+ - [Managing users](https://instantdb.com/docs/users.md): How to manage users in your Instant app.
345
+ - [Presence, Cursors, and Activity](https://instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
346
+ - [Instant CLI](https://instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
347
+ - [Storage](https://instantdb.com/docs/storage.md): How to upload and serve files with Instant.
348
+
349
+ # Final Note
350
+
351
+ Think before you answer. Make sure your code passes typechecks.
352
+ Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!
@@ -0,0 +1,352 @@
1
+ ---
2
+ trigger: always_on
3
+ description: How to use InstantDB
4
+ globs: ['**/*.tsx', '**/*.ts']
5
+ ---
6
+
7
+ Act as a world-class senior frontend engineer with deep expertise in InstantDB
8
+ and UI/UX design. Your primary goal is to generate complete and functional apps
9
+ with excellent visual asthetics using InstantDB as the backend.
10
+
11
+ # About InstantDB aka Instant
12
+
13
+ Instant is a client-side database (Modern Firebase) with built-in queries, transactions, auth, permissions, storage, real-time, and offline support.
14
+
15
+ # Instant SDKs
16
+
17
+ Instant provides client-side JS SDKs and an admin SDK:
18
+
19
+ - `@instantdb/core` --- vanilla JS
20
+ - `@instantdb/react` --- React
21
+ - `@instantdb/react-native` --- React Native / Expo
22
+ - `@instantdb/admin` --- backend scripts / servers
23
+
24
+ When installing, always check what package manager the project uses (npm, pnpm,
25
+ bun) first and then install the latest version of the Instant SDK.
26
+
27
+ # Managing Instant Apps
28
+
29
+ ## Prerequisites
30
+
31
+ Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
32
+ Look for an app id and admin token in `.env` or another env file.
33
+
34
+ If schema/perm files exist but the app id/admin token are missing, ask the user where to find them or whether to create a new app.
35
+
36
+ To create a new app:
37
+
38
+ ```bash
39
+ npx instant-cli init-without-files --title <APP_NAME>
40
+ ```
41
+
42
+ This outputs an app id and admin token. Store them in an env file.
43
+
44
+ If you have an app id/admin token but no schema/perm files, pull them:
45
+
46
+ ```bash
47
+ npx instant-cli pull --app <APP_ID> --token <ADMIN_TOKEN> --yes
48
+ ```
49
+
50
+ ## Schema changes
51
+
52
+ Edit `instant.schema.ts`, then push:
53
+
54
+ ```bash
55
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --yes
56
+ ```
57
+
58
+ New fields = additions; missing fields = deletions.
59
+
60
+ To rename fields:
61
+
62
+ ```bash
63
+ npx instant-cli push schema --app <APP_ID> --token <ADMIN_TOKEN> --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
64
+ ```
65
+
66
+ ## Permission changes
67
+
68
+ Edit `instant.perms.ts`, then push:
69
+
70
+ ```bash
71
+ npx instant-cli push perms --app <APP_ID> --token <ADMIN_TOKEN> --yes
72
+ ```
73
+
74
+ # CRITICAL Query Guidelines
75
+
76
+ CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
77
+
78
+ CRITICAL: You MUST index any field you want to filter or order by in the schema. If you do not, you will get an error when you try to filter or order by it.
79
+
80
+ Here is how ordering works:
81
+
82
+ ```
83
+ Ordering: order: { field: 'asc' | 'desc' }
84
+
85
+ Example: $: { order: { dueDate: 'asc' } }
86
+
87
+ Notes: - Field must be indexed + typed in schema
88
+ - Cannot order by nested attributes (e.g. 'owner.name')
89
+ ```
90
+
91
+ CRITICAL: Here is a concise summary of the `where` operator map which defines all the filtering options you can use with InstantDB queries to narrow results based on field values, comparisons, arrays, text patterns, and logical conditions.
92
+
93
+ ```
94
+ Equality: { field: value }
95
+
96
+ Inequality: { field: { $ne: value } }
97
+
98
+ Null checks: { field: { $isNull: true | false } }
99
+
100
+ Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
101
+
102
+ Sets: { field: { $in: [v1, v2] } }
103
+
104
+ Substring: { field: { $like: 'Get%' } } // case-sensitive
105
+ { field: { $ilike: '%get%' } } // case-insensitive
106
+
107
+ Logic: and: [ {...}, {...} ]
108
+ or: [ {...}, {...} ]
109
+
110
+ Nested fields: 'relation.field': value
111
+ ```
112
+
113
+ CRITICAL: The operator map above is the full set of `where` filters Instant
114
+ supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
115
+ `$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
116
+
117
+ CRITICAL: Pagination keys (`limit`, `offset`, `first`, `after`, `last`, `before`) only work on top-level namespaces. DO NOT use them on nested relations or else you will get an error.
118
+
119
+ CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
120
+
121
+ # CRITICAL Permission Guidelines
122
+
123
+ Below are some CRITICAL guidelines for writing permissions in InstantDB.
124
+
125
+ ## data.ref
126
+
127
+ - Use `data.ref("<path.to.attr>")` for linked attributes.
128
+ - Always returns a **list**.
129
+ - Must end with an **attribute**.
130
+
131
+ **Correct**
132
+
133
+ ```cel
134
+ auth.id in data.ref('post.author.id') // auth.id in list of author ids
135
+ data.ref('owner.id') == [] // there is no owner
136
+ ```
137
+
138
+ **Errors**
139
+
140
+ ```cel
141
+ auth.id in data.post.author.id
142
+ auth.id in data.ref('author')
143
+ data.ref('admins.id') == auth.id
144
+ auth.id == data.ref('owner.id')
145
+ data.ref('owner.id') == null
146
+ data.ref('owner.id').length > 0
147
+ ```
148
+
149
+ ## auth.ref
150
+
151
+ - Same as `data.ref` but path must start with `$user`.
152
+ - Returns a list.
153
+
154
+ **Correct**
155
+
156
+ ```cel
157
+ 'admin' in auth.ref('$user.role.type')
158
+ auth.ref('$user.role.type')[0] == 'admin'
159
+ ```
160
+
161
+ **Errors**
162
+
163
+ ```cel
164
+ auth.ref('role.type')
165
+ auth.ref('$user.role.type') == 'admin'
166
+ ```
167
+
168
+ ## Unsupported
169
+
170
+ ```cel
171
+ newData.ref('x')
172
+ data.ref(someVar + '.members.id')
173
+ ```
174
+
175
+ # Best Practices
176
+
177
+ ## Pass `schema` when initializing Instant
178
+
179
+ Always pass `schema` when initializing Instant to get type safety for queries and transactions
180
+
181
+ ```tsx
182
+ import schema from '@/instant.schema`
183
+
184
+ // On client
185
+ import { init } from '@instantdb/react'; // or your relevant Instant SDK
186
+ const clientDb = init({ appId, schema });
187
+
188
+ // On backend
189
+ import { init } from '@instantdb/admin';
190
+ const adminDb = init({ appId, adminToken, schema });
191
+ ```
192
+
193
+ ## Use `id()` to generate ids
194
+
195
+ Always use `id()` to generate ids for new entities
196
+
197
+ ```tsx
198
+ import { id } from '@instantdb/react'; // or your relevant Instant SDK
199
+ import { clientDb } from '@/lib/clientDb
200
+ clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
201
+ ```
202
+
203
+ ## Use Instant utility types for data models
204
+
205
+ Always use Instant utility types to type data models
206
+
207
+ ```tsx
208
+ import { AppSchema } from '@/instant.schema';
209
+
210
+ type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
211
+ type PostsWithProfile = InstaQLEntity<
212
+ AppSchema,
213
+ 'posts',
214
+ { author: { avatar: {} } }
215
+ >; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
216
+ ```
217
+
218
+ ## Use `db.useAuth` or `db.subscribeAuth` for auth state
219
+
220
+ ```tsx
221
+ import { clientDb } from '@/lib/clientDb';
222
+
223
+ // For react/react-native apps use db.useAuth
224
+ function App() {
225
+ const { isLoading, user, error } = clientDb.useAuth();
226
+ if (isLoading) { return null; }
227
+ if (error) { return <Error message={error.message /}></div>; }
228
+ if (user) { return <Main />; }
229
+ return <Login />;
230
+ }
231
+
232
+ // For vanilla JS apps use db.subscribeAuth
233
+ function App() {
234
+ renderLoading();
235
+ db.subscribeAuth((auth) => {
236
+ if (auth.error) { renderAuthError(auth.error.message); }
237
+ else if (auth.user) { renderLoggedInPage(auth.user); }
238
+ else { renderSignInPage(); }
239
+ });
240
+ }
241
+ ```
242
+
243
+ # Ad-hoc queries & transactions
244
+
245
+ Use `@instantdb/admin` to run ad-hoc queries and transactions on the backend.
246
+ Here is an example schema for a chat app along with seed and reset scripts.
247
+
248
+ ```tsx
249
+ // instant.schema.ts
250
+ const _schema = i.schema({
251
+ entities: {
252
+ $users: i.entity({
253
+ email: i.string().unique().indexed().optional(),
254
+ }),
255
+ profiles: i.entity({
256
+ displayName: i.string(),
257
+ }),
258
+ channels: i.entity({
259
+ name: i.string().indexed(),
260
+ }),
261
+ messages: i.entity({
262
+ content: i.string(),
263
+ timestamp: i.number().indexed(),
264
+ }),
265
+ },
266
+ links: {
267
+ userProfile: {
268
+ forward: { on: "profiles", has: "one", label: "user", onDelete: "cascade" }, // IMPORTANT: `cascade` can only be used in a has-one link
269
+ reverse: { on: "$users", has: "one", label: "profile" },
270
+ },
271
+ authorMessages: {
272
+ forward: { on: "messages", has: "one", label: "author", onDelete: "cascade" },
273
+ reverse: { on: "profiles", has: "many", label: "messages", },
274
+ },
275
+ channelMessages: {
276
+ forward: { on: "messages", has: "one", label: "channel", onDelete: "cascade" },
277
+ reverse: { on: "channels", has: "many", label: "messages" },
278
+ },
279
+ },
280
+ });
281
+
282
+ // scripts/seed.ts
283
+ import { id } from "@instantdb/admin";
284
+ import { adminDb } from "@/lib/adminDb";
285
+
286
+ const users: Record<string, User> = { ... }
287
+ const channels: Record<string, Channel> = { ... }
288
+ const mockMessages: Message[] = [ ... ]
289
+
290
+ function seed() {
291
+ console.log("Seeding db...");
292
+ const userTxs = Object.values(users).map(u => adminDb.tx.$users[u.id].create({}));
293
+ const profileTxs = Object.values(users).map(u => adminDb.tx.profiles[u.id].create({ displayName: u.displayName }).link({ user: u.id }));
294
+ const channelTxs = Object.values(channels).map(c => adminDb.tx.channels[c.id].create({ name: c.name }))
295
+ const messageTxs = mockMessages.map(m => {
296
+ const messageId = id();
297
+ return adminDb.tx.messages[messageId].create({
298
+ content: m.content,
299
+ timestamp: m.timestamp,
300
+ })
301
+ .link({ author: users[m.author].id })
302
+ .link({ channel: channels[m.channel].id });
303
+ })
304
+
305
+ adminDb.transact([...userTxs, ...profileTxs, ...channelTxs, ...messageTxs]);
306
+ }
307
+
308
+ seed();
309
+
310
+ // scripts/reset.ts
311
+ import { adminDb } from "@/lib/adminDb";
312
+
313
+ async function reset() {
314
+ console.log("Resetting database...");
315
+ const { $users, channels } = await adminDb.query({ $users: {}, channels: {} });
316
+
317
+ // Deleting all users will cascade delete profiles and messages
318
+ const userTxs = $users.map(user => adminDb.tx.$users[user.id].delete());
319
+
320
+ const channelTxs = channels.map(channel => adminDb.tx.channels[channel.id].delete());
321
+ adminDb.transact([...userTxs, ...channelTxs]);
322
+ }
323
+
324
+ reset();
325
+ ```
326
+
327
+ # Instant Documentation
328
+
329
+ The bullets below are links to the Instant documentation. They provide detailed information on how to use different features of InstantDB. Each line follows the pattern of
330
+
331
+ - [TOPIC](URL): Description of the topic.
332
+
333
+ Fetch the URL for a topic to learn more about it.
334
+
335
+ - [Common mistakes](https://instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
336
+ - [Initializing Instant](https://instantdb.com/docs/init.md): How to integrate Instant with your app.
337
+ - [Modeling data](https://instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
338
+ - [Writing data](https://instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
339
+ - [Reading data](https://instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
340
+ - [Instant on the Backend](https://instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
341
+ - [Patterns](https://instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
342
+ - [Auth](https://instantdb.com/docs/auth.md): Instant supports magic code, OAuth, Clerk, and custom auth.
343
+ - [Auth](https://instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
344
+ - [Managing users](https://instantdb.com/docs/users.md): How to manage users in your Instant app.
345
+ - [Presence, Cursors, and Activity](https://instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
346
+ - [Instant CLI](https://instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
347
+ - [Storage](https://instantdb.com/docs/storage.md): How to upload and serve files with Instant.
348
+
349
+ # Final Note
350
+
351
+ Think before you answer. Make sure your code passes typechecks.
352
+ Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!