create-instant-app 1.0.39 → 1.0.40
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 +18 -18
- package/package.json +4 -4
- package/template/rules/AGENTS.md +16 -6
- package/template/rules/cursor-rules.md +0 -432
- package/template/rules/windsurf-rules.md +0 -432
package/dist/index.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
Usage: npx create-instant-app my-app --yes`);return{...
|
|
2
|
+
import{Command as Fe,Option as l}from"commander";import*as U from"@clack/prompts";import{spawn as Oe}from"child_process";import{execa as je}from"execa";async function $(){return new Promise(e=>{let t=process.env.SHELL||"/bin/bash",n=Oe(t,["-i","-c","which claude"],{stdio:["ignore","pipe","ignore"]}),r="";n.stdout.on("data",i=>{r+=i.toString()}),n.on("close",i=>{if(i===0){let o=r.trim();o.includes("aliased to ")?e(o.split("aliased to ")[1]||null):e(o)}else e(null)})})}var J=async(e,t)=>{let n=await $();if(!n)throw new Error("Claude not found in path");await je(n,[e],{stdio:"inherit",cwd:t})};import{version as Me}from"@instantdb/version";import K from"path";var H=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e),Re=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,Y=e=>{let n=H(e).split("/"),r=n[n.length-1];if(r==="."){let s=K.resolve(process.cwd());r=K.basename(s)}let i=n.findIndex(s=>s.startsWith("@"));n.findIndex(s=>s.startsWith("@"))!==-1&&(r=n.slice(i).join("/"));let o=n.filter(s=>!s.startsWith("@")).join("/");return[r,o]},E=e=>e.trim(),D=e=>{let t=H(e),n=t.split("/"),r=n.findIndex(o=>o.startsWith("@")),i=n[n.length-1];if(n.findIndex(o=>o.startsWith("@"))!==-1&&(i=n.slice(r).join("/")),!(t==="."||Re.test(i??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};import{renderUnwrap as P,UI as m}from"instant-cli/ui";var C={base:"next-js-app-dir",appName:"Awesome Todos",ruleFiles:null,createRepo:!0,prompt:null,app:null,token:null,yes:!1,temporary:!1},q=e=>e.base||e.viteReact&&"vite-react"||e.vanilla&&"vite-vanilla"||e.next&&"next-js-app-dir"||e.expo&&"expo"||e.sv&&"sveltekit"||e.vue&&"vue-vite"||null,X=e=>e.cursor&&"cursor"||e.claude&&"claude"||e.codex&&"codex"||e.gemini&&"gemini"||e.rules&&"codex"||null,Z=async()=>{let e=C,t=new Fe().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 l("-b --base <template>","The base template to scaffold from").choices(["next-js-app-dir","vite-react","vite-vanilla","expo","bun-react","tanstack-start","tanstack-start-with-tanstack-query","solidjs-vite","sveltekit","vue-vite","vercel-ai-sdk","ai-chat"])).addOption(new l("-g --git","Create a git repo in the new project").default(!0)).addOption(new l("--expo","Use the Expo starter template").default(!1)).addOption(new l("--next","Use the NextJS starter template").default(!1)).addOption(new l("--vanilla","Use the vanilla JS starter template").default(!1)).addOption(new l("--vite-react","Use the Vite + React starter template").default(!1)).addOption(new l("--sv","Use the SvelteKit starter template").default(!1)).addOption(new l("--vue","Use the Vue + Vite starter template").default(!1)).addOption(new l("--no-git","Don't create a git repo in the new project")).addOption(new l("--cursor","Include a Cursor rules file in the scaffold")).addOption(new l("--claude","Include a CLAUDE.md file in the scaffold")).addOption(new l("--codex","Include an AGENTS.md file in the scaffold")).addOption(new l("--gemini","Include a GEMINI.md file in the scaffold")).addOption(new l("--rules","Include an AGENTS.md file in the scaffold")).addOption(new l("--ai","Create a new InstantDB app based off of a prompt. (requires Claude Code)")).addOption(new l("-a --app <app-id>","Link to an existing InstantDB app by ID (requires login or --token)")).addOption(new l("-t --token <token>","Auth token override (use with --app when not logged in)")).addOption(new l("-y --yes","Use all defaults (requires project name as first argument)").default(!1)).addOption(new l("--temp","Create a temporary app by default").default(!1)).version(Me).parse(process.argv),n=t.args[0]&&E(t.args[0]);if(n){let o=D(n);if(o)throw new Error("Invalid app name: "+o);e.appName=n}let r=t.opts();if(r.app&&r.temp)throw new Error("--app and --temp cannot be used together");if(r.yes){if(r.ai)throw new Error("--yes is not supported with --ai");if(!n)throw new Error(`When using --yes, you must specify a project name as the first argument.
|
|
3
|
+
Usage: npx create-instant-app my-app --yes`);return{...C,appName:n,base:q(r)??C.base,ruleFiles:X(r)??"claude",createRepo:r.git??C.createRepo,app:r.app??null,token:r.token??null,yes:!0,temporary:r.temp??!1}}if(r.ai&&!await $())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 U.group({appName:async()=>{if(n)return n.trim();let o=await P(new m.TextInput({prompt:"What will your project/folder be called?",placeholder:"awesome-todos",defaultValue:"awesome-todos",validate:a=>D(E(a)),modifyOutput:m.ciaModifier()}));return E(o)},prompt:async()=>r.ai?await P(new m.TextInput({prompt:"What would you like to create?",placeholder:"Create an app that...",modifyOutput:m.modifiers.piped([m.ciaModifier()])})):null,base:async({results:o})=>{let s=q(r);return s||(o.prompt?P(new m.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"vite-react",label:"Web: Vite React"},{value:"expo",label:"Mobile: Expo"}],defaultValue:"next-js-app-dir",modifyOutput:m.modifiers.piped([m.ciaModifier()])})):P(new m.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"expo",label:"Mobile: Expo"},{value:"vite-react",label:"Vite: React",secondary:!0},{value:"vite-vanilla",label:"Vite: Vanilla TS",secondary:!0},{value:"tanstack-start",label:"Tanstack Start",secondary:!0},{value:"bun-react",label:"Bun + React",secondary:!0},{value:"solidjs-vite",label:"Vite: SolidJS",secondary:!0},{value:"sveltekit",label:"SvelteKit",secondary:!0},{value:"vue-vite",label:"Vite: Vue",secondary:!0},{value:"vercel-ai-sdk",label:"Vercel AI SDK App Builder + SSR",secondary:!0},{value:"ai-chat",label:"Vercel AI SDK Chat App",secondary:!0}],defaultValue:"next-js-app-dir",modifyOutput:m.modifiers.piped([m.ciaModifier()])})))},ruleFiles:async({results:o})=>{if(o.prompt)return"claude";let s=X(r);return s||P(new m.Select({promptText:"Which AI tool would you like to add rule files for?",options:[{value:"claude",label:"Claude"},{value:"cursor",label:"Cursor"},{value:"codex",label:"Codex"},{value:"gemini",label:"Gemini"},{value:"zed",label:"Zed"},{value:"windsurf",label:"Windsurf"},{value:null,label:"None"}],defaultValue:"claude",modifyOutput:m.ciaModifier()}))},createRepo:async()=>r.git!==void 0?r.git:!0,temporary:async()=>r.temp??!1},{onCancel(){process.exit(1)}}),app:r.app??null,token:r.token??null,yes:!1}};import Se from"path";import Ne from"fs-extra";import{log as Ee,outro as Pt}from"@clack/prompts";import{intro as $e}from"@clack/prompts";import Q from"chalk";var De=` _ _ _
|
|
4
4
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 (_) | | | |
|
|
5
5
|
\u2588 \u2588\u2588\u2588\u2588 _ _ __ ___| |_ __ _ _ _ _| |_
|
|
6
6
|
\u2588 \u2588\u2588\u2588\u2588 | | '_ \\/ __| __/ _\\\`| '_ \\| __|
|
|
7
7
|
\u2588 \u2588\u2588\u2588\u2588 | | | | \\__ \\ || (_| | | | | |_
|
|
8
|
-
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 |_|_| |_|___/\\__\\__,_|_| |_|\\__|`,
|
|
9
|
-
`+
|
|
10
|
-
`).map(e=>`${
|
|
11
|
-
`))};import u from"path";import
|
|
12
|
-
enable-pre-post-scripts=true`),e.base==="expo"){let o=
|
|
13
|
-
Using git to clone from main...`),
|
|
8
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 |_|_| |_|___/\\__\\__,_|_| |_|\\__|`,ee=()=>{$e(`
|
|
9
|
+
`+De.split(`
|
|
10
|
+
`).map(e=>`${Q.gray("\u2502")}${Q.hex("#EA580D").bold(" "+e)}`).join(`
|
|
11
|
+
`))};import u from"path";import Ge from"tiged";import c from"fs-extra";import te from"path";import{fileURLToPath as Ue}from"url";var Ve=Ue(import.meta.url),Be=te.dirname(Ve),O=te.join(Be,"../");import I from"chalk";var T=e=>{if(e==="bun-react")return"bun";let t=process.env.npm_config_user_agent;return t?t.startsWith("yarn")?"yarn":t.startsWith("pnpm")?"pnpm":t.startsWith("bun")?"bun":"npm":"npm"};import{renderUnwrap as ne,UI as h}from"instant-cli/ui";import Le from"slugify";import We from"ignore";var re=async(e,t)=>{let n=u.resolve(process.cwd(),t);if(c.existsSync(n))if(c.readdirSync(n).length===0)h.log(`${I.cyan.bold(e.appName)} exists but is empty, continuing...`,h.ciaModifier(null));else{let o=await ne(new h.Select({promptText:I.redBright(`${I.bold("Warning:")} ${I.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:h.ciaModifier()}));o==="abort"&&(h.log("Aborting installation..."),process.exit(1)),o==="clear"&&c.emptyDirSync(n)}let r=Ke({projectDir:n,baseTemplateName:e.base}),i=e.appName==="."?"App":I.hex("#EA570B").bold(e.appName);if(await ne(new h.Spinner({promise:r,workingText:"Scaffolding project files...",doneText:`Successfully scaffolded ${i}!`,errorText:"Error scaffolding project files",modifyOutput:h.ciaModifier(null)})),c.pathExistsSync(u.join(n,"pnpm-lock.yaml"))&&c.removeSync(u.join(n,"pnpm-lock.yaml")),c.pathExistsSync(u.join(n,"bun.lock"))&&c.removeSync(u.join(n,"bun.lock")),T(e.base)==="pnpm"&&e.base==="expo"&&await c.appendFile(u.join(n,".npmrc"),`node-linker=hoisted
|
|
12
|
+
enable-pre-post-scripts=true`),e.base==="expo"){let o=Le.default(t);V(u.join(n,"app.json"),'"name": "expo-template"',`"name": "${t}"`),V(u.join(n,"app.json"),'"slug": "expo-template"',`"slug": "${o}"`),V(u.join(n,"app/_layout.tsx"),'"My Instant App"',`"${t}"`)}return n},V=(e,t,n)=>{let i=c.readFileSync(e,"utf8").replaceAll(t,n);c.writeFileSync(e,i)},ze=async({projectDir:e,baseTemplateName:t})=>{let n=`instantdb/instant/examples/${t}`;await Ge(n,{mode:"tar",disableCache:!0}).clone(e)};async function Je(e,t){let n=We();n.add(".git");try{let r=u.join(e,".gitignore"),i=await c.readFile(r,"utf8");n.add(i)}catch{}await c.copy(e,t,{filter:r=>{let i=u.relative(e,r);return i===""?!0:!n.ignores(i)}})}var Ke=async({projectDir:e,baseTemplateName:t})=>{let n=u.join(O,`template/base/${t}`),r=!!process.env.INSTANT_CLI_DEV&&!!process.env.INSTANT_REPO_FOLDER,i=c.pathExistsSync(n),o=r?"dev":i?"bundled-template":"tiged";if(o==="bundled-template"){c.copySync(n,e);let s=u.join(e,"_gitignore"),a=u.join(e,".gitignore");c.pathExistsSync(s)&&c.renameSync(s,a);return}if(o==="dev"){let s=process.env.INSTANT_REPO_FOLDER;if(!s)throw new Error("INSTANT_REPO_FOLDER is required when using repo-examples scaffolding.");let a=u.join(s,"examples",t);await Je(a,e);return}process.env.INSTANT_CLI_DEV&&!process.env.INSTANT_REPO_FOLDER&&h.log(I.bold.yellowBright(`WARNING: INSTANT_CLI_DEV is TRUE but no INSTANT_REPO_FOLDER is set.
|
|
13
|
+
Using git to clone from main...`),h.ciaModifier(null)),await ze({projectDir:e,baseTemplateName:t})};import{execa as He}from"execa";import{renderUnwrap as Ye,UI as j}from"instant-cli/ui";var oe=async(e,t)=>{let n=He(e,["install"],{cwd:t}),r=await Ye(new j.Spinner({promise:n,workingText:`Installing dependencies with ${e}...`,doneText:"Successfully installed dependencies!",modifyOutput:j.ciaModifier(null)}));r.exitCode!==0&&(j.log(r.stderr,j.ciaModifier(null)),process.exit(1))};import At from"chalk";import qe from"fs-extra";import ae from"path";var Xe={claude:"CLAUDE.md",gemini:"GEMINI.md",cursor:"AGENTS.md",codex:"AGENTS.md",zed:"AGENTS.md",windsurf:"AGENTS.md"},ie=({projectDir:e,ruleFilesToAdd:t})=>{t!==null&&qe.copyFileSync(ae.join(O,"template/rules/AGENTS.md"),ae.join(e,Xe[t]))};import{execSync as G}from"child_process";import B from"path";import*as pe from"@clack/prompts";import R from"chalk";import{execa as _}from"execa";import le from"fs-extra";import{renderUnwrap as se,UI as k}from"instant-cli/ui";var Ze=e=>{try{return G("git --version",{cwd:e}),!0}catch{return!1}},Qe=e=>le.existsSync(B.join(e,".git")),et=async e=>{try{return await _("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},tt=()=>{let t=G("git --version").toString().trim().split(" ")[2],n=t?.split(".")[0],r=t?.split(".")[1];return{major:Number(n),minor:Number(r)}},nt=()=>G("git config --global init.defaultBranch || echo main").toString().trim(),ce=async e=>{if(!Ze(e)){pe.log.warn("Git is not installed. Skipping Git initialization.");return}let t=Qe(e),n=await et(e),r=B.parse(e).name;if(n&&t){if(!await se(new k.Confirmation({promptText:`${R.redBright.bold("Warning:")} Git is already initialized in "${r}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,defaultValue:!1,modifyOutput:k.ciaModifier()})))return;le.removeSync(B.join(e,".git"))}else if(n&&!t&&!await se(new k.Confirmation({promptText:`${R.redBright.bold("Warning:")} "${r}" is already in a git worktree.
|
|
14
14
|
Would you still like to initialize a new git repository in this directory?
|
|
15
|
-
`,defaultValue:!1,modifyOutput:
|
|
16
|
-
INSTANT_APP_ADMIN_TOKEN=${r}`;
|
|
17
|
-
`),o=!0;i.forEach(s=>{if(s.length===0){o?(t(""),o=!1):console.log(
|
|
15
|
+
`,defaultValue:!1,modifyOutput:k.ciaModifier()})))return;try{let i=nt(),{major:o,minor:s}=tt();o<2||o==2&&s<28?(await _("git",["init"],{cwd:e}),await _("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await _("git",["init",`--initial-branch=${i}`],{cwd:e}),await _("git",["add","."],{cwd:e}),await _("git",["commit","-m","Initial commit (create-instant-app)"],{cwd:e}),k.log(R.dim(`${R.green("\u2713")} Git repository initialized successfully.`),k.ciaModifier(null))}catch{}};import ot from"env-paths";import{mkdir as at,readFile as it,writeFile as st}from"node:fs/promises";import pt from"open";import{join as lt}from"node:path";import{randomUUID as me}from"node:crypto";import{version as rt}from"@instantdb/version";var de=!!process.env.INSTANT_CLI_DEV,ue=de?"http://localhost:3000":"https://instantdb.com",L=process.env.INSTANT_CLI_API_URI||(de?"http://localhost:8888":"https://api.instantdb.com");async function w({path:e,body:t,method:n="GET",authToken:r,metadata:i}){let s={"Content-Type":"application/json","X-Instant-Source":"create-instant-app","X-Instant-Version":rt,"X-Instant-Command":"create"};r&&(s.Authorization=`Bearer ${r}`),i&&(s["X-Instant-Metadata"]=JSON.stringify(i));let a=await fetch(`${L}${e}`,{method:n??"GET",headers:s,body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(3e5)}),p;try{p=await a.json()}catch{p=null}if(!a.ok){let f=p.message||p.hint?.errors?.[0]?.message||"There was an error";throw new Error(f)}return p}import{renderUnwrap as A,UI as d}from"instant-cli/ui";function y(e){return e.replace(/[-_]/g," ").replace(/\b\w/g,t=>t.toUpperCase())}var ct=!!process.env.INSTANT_CLI_DEV,dt=!!process.env.INSTANT_CLI_FORCE_EPHEMERAL;function ge(){let e=`instantdb-${ct?"dev":"prod"}`,{config:t}=ot(e);return{authConfigFilePath:lt(t,"a"),appConfigDirPath:t}}var fe=async(e,t,n,r)=>{let i=me(),o=me(),s={id:i,title:e,admin_token:o,org_id:n};return r?.rules&&(s.rules=r.rules),r?.schema&&(s.schema=r.schema),await w({method:"POST",authToken:t,path:"/dash/apps",body:s,metadata:r}),{appId:i,adminToken:o,source:"created"}},W=(e,t,n)=>{w({method:"POST",path:`/dash/apps/${e}/track-import`,authToken:t,metadata:n}).catch(()=>{})},he=async e=>await w({method:"GET",path:"/dash",authToken:e}),we=async(e,t)=>await w({method:"GET",path:`/dash/orgs/${t}`,authToken:e});var ut=async(e,t)=>{try{let n=await he(e),r=n.apps.find(i=>i.id===t);if(r)return{appId:r.id,adminToken:r.admin_token};for(let i of n.orgs){let{apps:o}=await we(e,i.id),s=o.find(a=>a.id===t);if(s)return{appId:s.id,adminToken:s.admin_token}}return null}catch{return null}},mt=async(e,t)=>{try{return await w({method:"GET",path:`/dash/apps/${e}/schema/pull`,authToken:t}),!0}catch{return!1}},ft=async()=>dt?null:process.env.INSTANT_CLI_AUTH_TOKEN?process.env.INSTANT_CLI_AUTH_TOKEN:await it(ge().authConfigFilePath,"utf-8").catch(()=>null),F=async(e,t)=>{let n=await w({authToken:null,method:"POST",path:"/dash/apps/ephemeral",body:{title:e,rules:{$users:{view:"true"},$files:{allow:{view:"true",create:"true",delete:"true"}}}},metadata:t});return{appId:n.app.id,adminToken:n.app["admin-token"]}},ye=async(e,t,n)=>{let r=await ft();if(t?.yes&&!t?.app)if(r&&!t.temporary){let{appId:a,adminToken:p}=await fe(y(e),r,void 0,n);return{appId:a,adminToken:p,approach:"create"}}else{let{appId:a,adminToken:p}=await F(y(e),n);return{appId:a,adminToken:p,approach:"ephemeral"}}if(t?.temporary){let a=await A(new d.TextInput({defaultValue:y(e),prompt:"Enter a name for your temporary app:",placeholder:y(e),modifyOutput:d.ciaModifier()}));return{...await F(a,n),approach:"ephemeral"}}if(t?.app){if(t.token){if(!await mt(t.app,t.token))throw new Error("Invalid app ID and token combination. Please verify both the app ID and token are correct.");return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),W(t.app,t.token,n),{appId:t.app,adminToken:t.token,approach:"import"}}if(r){let a=await ut(r,t.app);if(!a)throw new Error(`You don't have access to app "${t.app}". Please check the app ID or use --token to provide a token.`);return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),W(a.appId,r,n),{appId:a.appId,adminToken:a.adminToken,approach:"import"}}throw new Error(`You must be logged in or provide --token when using --app. Either run 'npx instant-cli login' first, or use: --app ${t.app} --token <token>`)}if(!r){let a=await A(new d.Select({promptText:"You are not logged in.",options:[{label:"Login to your Instant account",value:"login"},{label:"Create a temporary app",value:"ephemeral"},{label:"Create an app later",value:"skip"}],modifyOutput:d.ciaModifier()}));if(a==="login"){let p=await w({authToken:null,method:"POST",path:"/dash/cli/auth/register"}).catch(Ce=>{throw new Error("Failed to register",{cause:Ce})}),{secret:f,ticket:g}=p;pt(`${ue}/dash?ticket=${g}`);let M=ht({secret:f}),z=await A(new d.Spinner({promise:M,workingText:"Waiting for login in browser",disappearWhenDone:!0,modifyOutput:d.ciaModifier(null)}));await wt(z.token),r=z.token}if(a==="skip")return d.log("Skipping app link step",d.ciaModifier(null)),null;if(a==="ephemeral"){let p=await A(new d.TextInput({defaultValue:y(e),prompt:"Enter a name for your temporary app:",placeholder:y(e),modifyOutput:d.ciaModifier()}));return{...await F(p,n),approach:"ephemeral"}}}if(!r)return null;let i=await he(r).catch(a=>{throw new Error("Failed to fetch dashboard",{cause:a})}),o=i.orgs.filter(a=>a.role!=="app-member");i.orgs=o;let s=await A(new d.AppSelector({startingMenuIndex:0,defaultAppName:y(e),allowCreate:!0,allowEphemeral:!0,api:{getDash(){return i},createEphemeralApp(a){return F(a,n)},getAppsForOrg:async a=>{let{apps:p}=await we(r,a);return{apps:p}},createApp:async(a,p)=>{let{appId:f,adminToken:g}=await fe(a,r,p,n);return{appId:f,adminToken:g}}},modifyOutput:d.ciaModifier()}));return s?.approach==="import"&&W(s.appId,r,n),s};function gt(e){return new Promise(t=>setTimeout(t,e))}async function ht({secret:e}){for(let t=1;t<=120;t++){await gt(1e3);let n=await fetch(`${L}/dash/cli/auth/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:e})});if(!(!n.ok&&(await n.json()).hint.errors?.[0]?.issue==="waiting-for-user")&&n.ok)return n.json()}throw new Error("Timed out waiting for login")}async function wt(e){let t=ge();return await at(t.appConfigDirPath,{recursive:!0}),st(t.authConfigFilePath,e,"utf-8")}import yt from"fs-extra";import Tt from"path";var bt={"next-js-app-dir":"NEXT_PUBLIC_INSTANT_APP_ID","vite-react":"VITE_INSTANT_APP_ID","vite-vanilla":"VITE_INSTANT_APP_ID",expo:"EXPO_PUBLIC_INSTANT_APP_ID","tanstack-start":"VITE_INSTANT_APP_ID","bun-react":"BUN_PUBLIC_INSTANT_APP_ID","solidjs-vite":"VITE_INSTANT_APP_ID",sveltekit:"VITE_INSTANT_APP_ID","vue-vite":"VITE_INSTANT_APP_ID","tanstack-start-with-tanstack-query":"VITE_INSTANT_APP_ID","vercel-ai-sdk":"NEXT_PUBLIC_INSTANT_APP_ID","ai-chat":"NEXT_PUBLIC_INSTANT_APP_ID"},Te=(e,t,n,r)=>{let i=Tt.join(t,".env"),s=`${bt[e.base]}=${n}
|
|
16
|
+
INSTANT_APP_ADMIN_TOKEN=${r}`;yt.writeFileSync(i,s)};import{stdin as b,stdout as be}from"process";import{setRawModeWindowsFriendly as Ie}from"instant-cli/ui";async function It(){return new Promise(e=>{if(!be.isTTY||!b.isTTY){e(null);return}let t=b.isRaw;t||Ie(b,!0);let n=setTimeout(()=>{i(),e(null)},100),r="",i=()=>{clearTimeout(n),b.removeListener("data",o),!t&&b.isTTY&&Ie(b,!1)},o=s=>{let a=s.toString();r+=a;let p=r.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x1b\\|\x07)/);if(p){i();let[,f,g,M]=p;e(`rgb:${f}/${g}/${M}`)}};b.on("data",o),be.write("\x1B]11;?\x1B\\")})}function kt(e){let t=e.match(/rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);if(!t)return null;let[,n,r,i]=t,o=Math.round(parseInt(n,16)/257),s=Math.round(parseInt(r,16)/257),a=Math.round(parseInt(i,16)/257);return{r:o,g:s,b:a}}function _t(e){return(.299*e.r+.587*e.g+.114*e.b)/255>.5}async function ke(){try{let e=await It();if(!e)return"unknown";let t=kt(e);return t?_t(t)?"light":"dark":"unknown"}catch(e){return console.error("Error detecting terminal theme:",e),"unknown"}}import S from"chalk";var x=(e,t)=>e==="light"?S.bgYellowBright(t):S.bgBlackBright(t),_e=(e,t=console.log,n=!1)=>{let r=(process.stdout.columns||80)-4,i=e.split(`
|
|
17
|
+
`),o=!0;i.forEach(s=>{if(s.length===0){o?(t(""),o=!1):console.log(S.gray("\u2502"));return}for(let a=0;a<s.length;a+=r){let p=s.slice(a,a+r);a+r<s.length&&p.length===r&&(p=p+"-"),o?(t(p),o=!1):console.log(S.gray("\u2502 ")+p)}}),n&&console.log(S.gray("\u2502"))};var N="\x1B[?25h";import{execa as St}from"execa";import xe from"fs-extra";import{permsTypescriptFileToCode as xt,schemaTypescriptFileToInstantSchema as vt}from"@instantdb/platform";function ve(e){for(let t of["src/instant.perms.ts","instant.perms.ts"])try{let n=xe.readFileSync(`${e}/${t}`,"utf8");return{code:xt(n,t)}}catch{}return null}function Pe(e){for(let t of["src/instant.schema.ts","instant.schema.ts"])try{let n=xe.readFileSync(`${e}/${t}`,"utf8");return vt(n,t)}catch{}return null}import{UI as v}from"instant-cli/ui";var Ae=e=>{if(e){if(e.approach==="import"){v.log(`Imported app with App ID: ${e.appId}`,v.ciaModifier());return}e.approach==="create"&&v.log(`Created new app with App ID: ${e.appId}`,v.ciaModifier()),e.approach==="ephemeral"&&v.log(`Created temporary app with App ID: ${e.appId}`,v.ciaModifier())}};var Nt=async()=>{process.argv.some(g=>["-h","--help","--version","-V"].includes(g))||ee();let e=await ke(),t=await Z(),[n,r]=Y(t.appName),i=T(t.base),o=await re(t,r);ie({projectDir:o,ruleFilesToAdd:t.ruleFiles});let s={template:t.base,aiTool:t.ruleFiles??"none",usedAiPrompt:!!t.prompt,rules:ve(o),schema:Pe(o)},a=await ye(r==="."?n:r,t,s);Ae(a),a&&Te(t,o,a.appId,a.adminToken);let p=Ne.readJSONSync(Se.join(o,"package.json"));if(p.name=n,i!=="bun"){let{stdout:g}=await St(i,["-v"],{cwd:o});p.packageManager=`${i}@${g.trim()}`}Ne.writeJSONSync(Se.join(o,"package.json"),p,{spaces:2}),await oe(T(t.base),o),t.createRepo&&await ce(o),t.prompt&&(await J(t.prompt,o),process.stdout.write(N)),Pt("Done!");let f=t.base==="expo"?"start":"dev";a?(console.log(`
|
|
18
18
|
\u{1F389} Success! Your project is ready to go!
|
|
19
19
|
|
|
20
20
|
To get started:
|
|
21
|
-
1. ${
|
|
22
|
-
2. ${
|
|
21
|
+
1. ${x(e,"cd "+r)}
|
|
22
|
+
2. ${x(e,T(t.base)+" run "+f)}
|
|
23
23
|
`),a.approach==="ephemeral"&&console.log(`
|
|
24
24
|
An ephemeral app has been created and added to your .env file.
|
|
25
|
-
It will expire in two weeks. For a permanent app, sign in and use ${
|
|
25
|
+
It will expire in two weeks. For a permanent app, sign in and use ${x(e,"npx instant-cli claim")}
|
|
26
26
|
`)):console.log(`
|
|
27
27
|
\u{1F389} Success! Your project is ready to go!
|
|
28
28
|
|
|
29
29
|
To get started:
|
|
30
|
-
1. ${
|
|
31
|
-
2. Create a new app on ${
|
|
30
|
+
1. ${x(e,"cd "+r)}
|
|
31
|
+
2. Create a new app on ${At.underline("www.instantdb.com")}
|
|
32
32
|
3. Add your APP_ID to the .env file
|
|
33
|
-
4. ${
|
|
34
|
-
`),process.stdout.write(
|
|
33
|
+
4. ${x(e,T(t.base)+" run "+f)}
|
|
34
|
+
`),process.stdout.write(N),process.exit(0)};Nt().catch(e=>{Ee.error("Aborting installation..."),_e(e.message,Ee.error),process.stdout.write(N),process.exit(1)});process.on("SIGINT",()=>{process.stdout.write(N),process.exit(0)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-instant-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
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": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"slugify": "^1.6.6",
|
|
36
36
|
"sort-package-json": "^2.10.0",
|
|
37
37
|
"tiged": "^2.12.7",
|
|
38
|
-
"@instantdb/
|
|
39
|
-
"@instantdb/
|
|
40
|
-
"instant-cli": "1.0.
|
|
38
|
+
"@instantdb/version": "1.0.40",
|
|
39
|
+
"@instantdb/platform": "1.0.40",
|
|
40
|
+
"instant-cli": "1.0.40"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@anthropic-ai/sdk": "^0.60.0",
|
package/template/rules/AGENTS.md
CHANGED
|
@@ -344,9 +344,15 @@ import { clientDb } from '@/lib/clientDb';
|
|
|
344
344
|
// For react/react-native apps use db.useAuth
|
|
345
345
|
function App() {
|
|
346
346
|
const { isLoading, user, error } = clientDb.useAuth();
|
|
347
|
-
if (isLoading) {
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
if (isLoading) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (error) {
|
|
351
|
+
return <Error message={error.message} />;
|
|
352
|
+
}
|
|
353
|
+
if (user) {
|
|
354
|
+
return <Main />;
|
|
355
|
+
}
|
|
350
356
|
return <Login />;
|
|
351
357
|
}
|
|
352
358
|
|
|
@@ -354,9 +360,13 @@ function App() {
|
|
|
354
360
|
function App() {
|
|
355
361
|
renderLoading();
|
|
356
362
|
db.subscribeAuth((auth) => {
|
|
357
|
-
if (auth.error) {
|
|
358
|
-
|
|
359
|
-
else
|
|
363
|
+
if (auth.error) {
|
|
364
|
+
renderAuthError(auth.error.message);
|
|
365
|
+
} else if (auth.user) {
|
|
366
|
+
renderLoggedInPage(auth.user);
|
|
367
|
+
} else {
|
|
368
|
+
renderSignInPage();
|
|
369
|
+
}
|
|
360
370
|
});
|
|
361
371
|
}
|
|
362
372
|
```
|
|
@@ -1,432 +0,0 @@
|
|
|
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 aesthetics 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/solidjs` --- SolidJS
|
|
23
|
-
- `@instantdb/svelte` --- Svelte
|
|
24
|
-
- `@instantdb/vue` --- Vue
|
|
25
|
-
- `@instantdb/admin` --- backend scripts / servers
|
|
26
|
-
|
|
27
|
-
When installing, always check what package manager the project uses (npm, pnpm,
|
|
28
|
-
bun) first and then install the latest version of the Instant SDK. If working in
|
|
29
|
-
React use Next and Tailwind unless specified otherwise.
|
|
30
|
-
|
|
31
|
-
# Managing Instant Apps
|
|
32
|
-
|
|
33
|
-
## Prerequisites
|
|
34
|
-
|
|
35
|
-
Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
|
|
36
|
-
Look for an app id and admin token in `.env` or another env file.
|
|
37
|
-
|
|
38
|
-
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.
|
|
39
|
-
|
|
40
|
-
To create a new app:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npx instant-cli init-without-files --title <APP_NAME>
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This outputs an app id and admin token. Store them in an env file.
|
|
47
|
-
|
|
48
|
-
If you get an error related to not being logged in tell the user to:
|
|
49
|
-
|
|
50
|
-
- Sign up for free or log in at https://instantdb.com
|
|
51
|
-
- Then run `npx instant-cli login` to authenticate the CLI
|
|
52
|
-
- Then re-run the init command
|
|
53
|
-
|
|
54
|
-
If you have an app id/admin token but no schema/perm files, pull them:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npx instant-cli pull --yes
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Schema changes
|
|
61
|
-
|
|
62
|
-
Edit `instant.schema.ts`, then push:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
npx instant-cli push schema --yes
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
New fields = additions; missing fields = deletions.
|
|
69
|
-
|
|
70
|
-
To rename fields:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npx instant-cli push schema --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Permission changes
|
|
77
|
-
|
|
78
|
-
Edit `instant.perms.ts`, then push:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx instant-cli push perms --yes
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
# CRITICAL Query Guidelines
|
|
85
|
-
|
|
86
|
-
CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
|
|
87
|
-
|
|
88
|
-
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.
|
|
89
|
-
|
|
90
|
-
Here is how ordering works:
|
|
91
|
-
|
|
92
|
-
```text
|
|
93
|
-
Ordering: order: { field: 'asc' | 'desc' }
|
|
94
|
-
|
|
95
|
-
Example: $: { order: { dueDate: 'asc' } }
|
|
96
|
-
|
|
97
|
-
Notes: - Field must be indexed + typed in schema
|
|
98
|
-
- Cannot order by nested attributes (e.g. 'owner.name')
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
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.
|
|
102
|
-
|
|
103
|
-
```text
|
|
104
|
-
Equality: { field: value }
|
|
105
|
-
|
|
106
|
-
Inequality: { field: { $ne: value } }
|
|
107
|
-
|
|
108
|
-
Null checks: { field: { $isNull: true | false } }
|
|
109
|
-
|
|
110
|
-
Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
|
|
111
|
-
|
|
112
|
-
Sets: { field: { $in: [v1, v2] } }
|
|
113
|
-
|
|
114
|
-
Substring: { field: { $like: 'Get%' } } // case-sensitive
|
|
115
|
-
{ field: { $ilike: '%get%' } } // case-insensitive
|
|
116
|
-
|
|
117
|
-
Logic: and: [ {...}, {...} ]
|
|
118
|
-
or: [ {...}, {...} ]
|
|
119
|
-
|
|
120
|
-
Nested fields: 'relation.field': value
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
CRITICAL: The operator map above is the full set of `where` filters Instant
|
|
124
|
-
supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
|
|
125
|
-
`$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
|
|
126
|
-
|
|
127
|
-
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.
|
|
128
|
-
|
|
129
|
-
CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
|
|
130
|
-
|
|
131
|
-
# CRITICAL Permission Guidelines
|
|
132
|
-
|
|
133
|
-
Below are some CRITICAL guidelines for writing permissions in InstantDB.
|
|
134
|
-
|
|
135
|
-
## `data.ref`
|
|
136
|
-
|
|
137
|
-
- Use `data.ref("<path.to.attr>")` for linked attributes.
|
|
138
|
-
- Always returns a **list**.
|
|
139
|
-
- Must end with an **attribute**.
|
|
140
|
-
|
|
141
|
-
**Correct**
|
|
142
|
-
|
|
143
|
-
```cel
|
|
144
|
-
auth.id in data.ref('post.author.id') // auth.id in list of author ids
|
|
145
|
-
data.ref('owner.id') == [] // there is no owner
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Errors**
|
|
149
|
-
|
|
150
|
-
```cel
|
|
151
|
-
auth.id in data.post.author.id
|
|
152
|
-
auth.id in data.ref('author')
|
|
153
|
-
data.ref('admins.id') == auth.id
|
|
154
|
-
auth.id == data.ref('owner.id')
|
|
155
|
-
data.ref('owner.id') == null
|
|
156
|
-
data.ref('owner.id').length > 0
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## `auth.ref`
|
|
160
|
-
|
|
161
|
-
- Same as `data.ref` but path must start with `$user`.
|
|
162
|
-
- Returns a list.
|
|
163
|
-
|
|
164
|
-
**Correct**
|
|
165
|
-
|
|
166
|
-
```cel
|
|
167
|
-
'admin' in auth.ref('$user.role.type')
|
|
168
|
-
auth.ref('$user.role.type')[0] == 'admin'
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Errors**
|
|
172
|
-
|
|
173
|
-
```cel
|
|
174
|
-
auth.ref('role.type')
|
|
175
|
-
auth.ref('$user.role.type') == 'admin'
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Unsupported
|
|
179
|
-
|
|
180
|
-
```cel
|
|
181
|
-
newData.ref('x')
|
|
182
|
-
data.ref(someVar + '.members.id')
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## $users Permissions
|
|
186
|
-
|
|
187
|
-
- Default `view` permission is `auth.id == data.id`
|
|
188
|
-
- Default `update` and `delete` permissions is false
|
|
189
|
-
- Default `create` permission is true (anyone can sign up)
|
|
190
|
-
- Can override `view`, `update`, and `create`
|
|
191
|
-
- Cannot override `delete`
|
|
192
|
-
- The `create` rule runs during auth signup flows (not via `transact`). Use it to restrict signups or validate `extraFields`.
|
|
193
|
-
- `extraFields` require an explicit `create` rule. Without one, signup is blocked to prevent unvalidated writes.
|
|
194
|
-
|
|
195
|
-
## $files Permissions
|
|
196
|
-
|
|
197
|
-
- Default permissions are all false. Override as needed to allow access.
|
|
198
|
-
- `data.ref` does not work for `$files` permissions.
|
|
199
|
-
- Use `data.path.startsWith(...)` or `data.path.endsWith(...)` to write
|
|
200
|
-
path-based rules.
|
|
201
|
-
|
|
202
|
-
## Field-level Permissions
|
|
203
|
-
|
|
204
|
-
Restrict access to specific fields while keeping the entity public:
|
|
205
|
-
|
|
206
|
-
```json
|
|
207
|
-
{
|
|
208
|
-
"$users": {
|
|
209
|
-
"allow": {
|
|
210
|
-
"view": "true"
|
|
211
|
-
},
|
|
212
|
-
"fields": {
|
|
213
|
-
"email": "auth.id == data.id"
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Notes:
|
|
220
|
-
|
|
221
|
-
- Field rules override entity-level `view` for that field
|
|
222
|
-
- Useful for hiding sensitive data (emails, phone numbers) on public entities
|
|
223
|
-
|
|
224
|
-
# CRITICAL Storage Guidelines
|
|
225
|
-
|
|
226
|
-
CRITICAL: If an app displays images or files, use Instant Storage. Do not store
|
|
227
|
-
URLs as string attributes on your entities. This includes seed scripts: do not
|
|
228
|
-
use placeholder image URLs (e.g. picsum.photos) as string attributes to fake
|
|
229
|
-
file support.
|
|
230
|
-
|
|
231
|
-
Uploads auto-create `$files` entities. Link them to your data via the schema,
|
|
232
|
-
then query through the relationship to get URLs.
|
|
233
|
-
|
|
234
|
-
CRITICAL: You MUST include `$files` in your schema entities if you use Storage.
|
|
235
|
-
|
|
236
|
-
CRITICAL: `$files` entities can only be created via `db.storage.uploadFile`. You
|
|
237
|
-
cannot create `$files` via `db.transact`, and you cannot set `url` via transactions.
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
entities: {
|
|
241
|
-
$files: i.entity({
|
|
242
|
-
path: i.string().unique().indexed(),
|
|
243
|
-
url: i.string(),
|
|
244
|
-
}),
|
|
245
|
-
posts: i.entity({
|
|
246
|
-
caption: i.string(),
|
|
247
|
-
}),
|
|
248
|
-
},
|
|
249
|
-
links: {
|
|
250
|
-
postImage: {
|
|
251
|
-
forward: { on: "posts", has: "one", label: "image" },
|
|
252
|
-
reverse: { on: "$files", has: "many", label: "posts" },
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Upload and link the returned file ID to your entity
|
|
257
|
-
const postId = id();
|
|
258
|
-
const { data } = await db.storage.uploadFile(`posts/${postId}/${file.name}`, file);
|
|
259
|
-
db.transact(
|
|
260
|
-
db.tx.posts[postId].update({ caption }).link({ image: data.id })
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// Query through the relationship to get the URL
|
|
264
|
-
const { data } = db.useQuery({ posts: { image: {} } });
|
|
265
|
-
<img src={post.image.url} />
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
# CRITICAL Rooms Guidelines
|
|
269
|
-
|
|
270
|
-
CRITICAL: Hooks for presence and topics live on `db.rooms` and take the room as the first arg. The room object itself has no `usePresence` or `publishPresence` methods.
|
|
271
|
-
|
|
272
|
-
Rooms host two ephemeral primitives: presence (cursor positions, who's online) and topics (live reactions). Use them only for data that should NOT persist. Persisted data via `transact` already syncs in real-time to all subscribed clients, so reach for rooms only when the data is intentionally ephemeral.
|
|
273
|
-
|
|
274
|
-
## Presence
|
|
275
|
-
|
|
276
|
-
Each peer publishes a presence object readable by all other peers in the room. Retained for the connection and cleaned up automatically on disconnect.
|
|
277
|
-
|
|
278
|
-
```tsx
|
|
279
|
-
const room = db.room('chat', 'main');
|
|
280
|
-
const { user, peers, publishPresence } = db.rooms.usePresence(room, {
|
|
281
|
-
initialPresence: { x: 0, y: 0 },
|
|
282
|
-
});
|
|
283
|
-
// peers is keyed by peerId, not an array. Use Object.values(peers) to iterate
|
|
284
|
-
publishPresence({ x: 50, y: 50 });
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
## Topics
|
|
288
|
-
|
|
289
|
-
Topic payloads aren't retained. Peers only see events fired while they're listening.
|
|
290
|
-
|
|
291
|
-
```tsx
|
|
292
|
-
const room = db.room('chat', 'main');
|
|
293
|
-
|
|
294
|
-
const publishEmoji = db.rooms.usePublishTopic(room, 'emoji');
|
|
295
|
-
publishEmoji({ name: 'fire' });
|
|
296
|
-
|
|
297
|
-
db.rooms.useTopicEffect(room, 'emoji', (payload) => {
|
|
298
|
-
animateEmoji(payload.name);
|
|
299
|
-
});
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
# Best Practices
|
|
303
|
-
|
|
304
|
-
## Pass `schema` when initializing Instant
|
|
305
|
-
|
|
306
|
-
Always pass `schema` when initializing Instant to get type safety for queries and transactions
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
import schema from '@/instant.schema';
|
|
310
|
-
|
|
311
|
-
// On client
|
|
312
|
-
import { init } from '@instantdb/react'; // or your relevant Instant SDK
|
|
313
|
-
const clientDb = init({ appId, schema });
|
|
314
|
-
|
|
315
|
-
// On backend
|
|
316
|
-
import { init } from '@instantdb/admin';
|
|
317
|
-
const adminDb = init({ appId, adminToken, schema });
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## Use `id()` to generate ids
|
|
321
|
-
|
|
322
|
-
Always use `id()` to generate ids for new entities
|
|
323
|
-
|
|
324
|
-
```tsx
|
|
325
|
-
import { id } from '@instantdb/react'; // or your relevant Instant SDK
|
|
326
|
-
import { clientDb } from '@/lib/clientDb';
|
|
327
|
-
clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## Use Instant utility types for data models
|
|
331
|
-
|
|
332
|
-
Always use Instant utility types to type data models
|
|
333
|
-
|
|
334
|
-
```tsx
|
|
335
|
-
import { AppSchema } from '@/instant.schema';
|
|
336
|
-
|
|
337
|
-
type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
|
|
338
|
-
type PostsWithProfile = InstaQLEntity<
|
|
339
|
-
AppSchema,
|
|
340
|
-
'posts',
|
|
341
|
-
{ author: { avatar: {} } }
|
|
342
|
-
>; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## Use `db.useAuth` or `db.subscribeAuth` for auth state
|
|
346
|
-
|
|
347
|
-
```tsx
|
|
348
|
-
import { clientDb } from '@/lib/clientDb';
|
|
349
|
-
|
|
350
|
-
// For react/react-native apps use db.useAuth
|
|
351
|
-
function App() {
|
|
352
|
-
const { isLoading, user, error } = clientDb.useAuth();
|
|
353
|
-
if (isLoading) { return null; }
|
|
354
|
-
if (error) { return <Error message={error.message} />; }
|
|
355
|
-
if (user) { return <Main />; }
|
|
356
|
-
return <Login />;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// For vanilla JS apps use db.subscribeAuth
|
|
360
|
-
function App() {
|
|
361
|
-
renderLoading();
|
|
362
|
-
db.subscribeAuth((auth) => {
|
|
363
|
-
if (auth.error) { renderAuthError(auth.error.message); }
|
|
364
|
-
else if (auth.user) { renderLoggedInPage(auth.user); }
|
|
365
|
-
else { renderSignInPage(); }
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
## Set custom properties at signup with `extraFields`
|
|
371
|
-
|
|
372
|
-
Pass `extraFields` to any sign-in method to write custom `$users` properties atomically on user creation.
|
|
373
|
-
Fields must be defined as optional attrs on `$users` in your schema.
|
|
374
|
-
Use the `created` boolean to scaffold data for new users.
|
|
375
|
-
|
|
376
|
-
```tsx
|
|
377
|
-
// Set properties at signup
|
|
378
|
-
const { user, created } = await db.auth.signInWithMagicCode({
|
|
379
|
-
email,
|
|
380
|
-
code,
|
|
381
|
-
extraFields: { nickname, createdAt: Date.now() },
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Scaffold data for new users
|
|
385
|
-
if (created) {
|
|
386
|
-
db.transact([
|
|
387
|
-
db.tx.settings[id()]
|
|
388
|
-
.update({ theme: 'light', notifications: true })
|
|
389
|
-
.link({ user: user.id }),
|
|
390
|
-
]);
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
# Ad-hoc queries from the CLI
|
|
395
|
-
|
|
396
|
-
Run `npx instant-cli query '{ posts: {} }' --admin` to query your app. A context flag is required: `--admin`, `--as-email <email>`, or `--as-guest`. Also supports `--app <id>`.
|
|
397
|
-
|
|
398
|
-
# Instant Documentation
|
|
399
|
-
|
|
400
|
-
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
|
|
401
|
-
|
|
402
|
-
- [TOPIC](URL): Description of the topic.
|
|
403
|
-
|
|
404
|
-
Fetch the URL for a topic to learn more about it.
|
|
405
|
-
|
|
406
|
-
- [Common mistakes](https://www.instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
|
|
407
|
-
- [Initializing Instant](https://www.instantdb.com/docs/init.md): How to integrate Instant with your app.
|
|
408
|
-
- [Modeling data](https://www.instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
|
|
409
|
-
- [Writing data](https://www.instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
|
|
410
|
-
- [Reading data](https://www.instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
|
|
411
|
-
- [Instant on the Backend](https://www.instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
|
|
412
|
-
- [Patterns](https://www.instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
|
|
413
|
-
- [Auth](https://www.instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
|
|
414
|
-
- [Guest Auth](https://www.instantdb.com/docs/auth/guest-auth.md): How to add guest auth to your Instant app.
|
|
415
|
-
- [Other Auth](https://www.instantdb.com/docs/auth.md): Additional auth methods supported by Instant.
|
|
416
|
-
- [Managing users](https://www.instantdb.com/docs/users.md): How to manage users in your Instant app.
|
|
417
|
-
- [Presence, Cursors, and Activity](https://www.instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
|
|
418
|
-
- [Instant CLI](https://www.instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
|
|
419
|
-
- [Storage](https://www.instantdb.com/docs/storage.md): How to upload and serve files with Instant.
|
|
420
|
-
- [Streams](https://www.instantdb.com/docs/streams.md): How to use streams with Instant.
|
|
421
|
-
- [Stripe Payments](https://www.instantdb.com/docs/stripe-payments.md): How to integrate Stripe payments with Instant.
|
|
422
|
-
- [React Native](https://www.instantdb.com/docs/start-rn.md): How to use Instant in React Native apps.
|
|
423
|
-
- [Vanilla JS](https://www.instantdb.com/docs/start-vanilla.md): How to use Instant in vanilla JS apps.
|
|
424
|
-
- [SolidJS](https://www.instantdb.com/docs/start-solidjs.md): How to use Instant in SolidJS apps.
|
|
425
|
-
- [Svelte](https://www.instantdb.com/docs/start-svelte.md): How to use Instant in Svelte apps.
|
|
426
|
-
- [Vue](https://www.instantdb.com/docs/start-vue.md): How to use Instant in Vue apps.
|
|
427
|
-
- [TanStack](https://www.instantdb.com/docs/start-tanstack.md): How to use Instant in TanStack apps.
|
|
428
|
-
|
|
429
|
-
# Final Note
|
|
430
|
-
|
|
431
|
-
Think before you answer. Make sure your code passes typechecks `tsc --noEmit` and works as expected.
|
|
432
|
-
Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!
|
|
@@ -1,432 +0,0 @@
|
|
|
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 aesthetics 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/solidjs` --- SolidJS
|
|
23
|
-
- `@instantdb/svelte` --- Svelte
|
|
24
|
-
- `@instantdb/vue` --- Vue
|
|
25
|
-
- `@instantdb/admin` --- backend scripts / servers
|
|
26
|
-
|
|
27
|
-
When installing, always check what package manager the project uses (npm, pnpm,
|
|
28
|
-
bun) first and then install the latest version of the Instant SDK. If working in
|
|
29
|
-
React use Next and Tailwind unless specified otherwise.
|
|
30
|
-
|
|
31
|
-
# Managing Instant Apps
|
|
32
|
-
|
|
33
|
-
## Prerequisites
|
|
34
|
-
|
|
35
|
-
Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
|
|
36
|
-
Look for an app id and admin token in `.env` or another env file.
|
|
37
|
-
|
|
38
|
-
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.
|
|
39
|
-
|
|
40
|
-
To create a new app:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npx instant-cli init-without-files --title <APP_NAME>
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This outputs an app id and admin token. Store them in an env file.
|
|
47
|
-
|
|
48
|
-
If you get an error related to not being logged in tell the user to:
|
|
49
|
-
|
|
50
|
-
- Sign up for free or log in at https://instantdb.com
|
|
51
|
-
- Then run `npx instant-cli login` to authenticate the CLI
|
|
52
|
-
- Then re-run the init command
|
|
53
|
-
|
|
54
|
-
If you have an app id/admin token but no schema/perm files, pull them:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npx instant-cli pull --yes
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Schema changes
|
|
61
|
-
|
|
62
|
-
Edit `instant.schema.ts`, then push:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
npx instant-cli push schema --yes
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
New fields = additions; missing fields = deletions.
|
|
69
|
-
|
|
70
|
-
To rename fields:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npx instant-cli push schema --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Permission changes
|
|
77
|
-
|
|
78
|
-
Edit `instant.perms.ts`, then push:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx instant-cli push perms --yes
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
# CRITICAL Query Guidelines
|
|
85
|
-
|
|
86
|
-
CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
|
|
87
|
-
|
|
88
|
-
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.
|
|
89
|
-
|
|
90
|
-
Here is how ordering works:
|
|
91
|
-
|
|
92
|
-
```text
|
|
93
|
-
Ordering: order: { field: 'asc' | 'desc' }
|
|
94
|
-
|
|
95
|
-
Example: $: { order: { dueDate: 'asc' } }
|
|
96
|
-
|
|
97
|
-
Notes: - Field must be indexed + typed in schema
|
|
98
|
-
- Cannot order by nested attributes (e.g. 'owner.name')
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
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.
|
|
102
|
-
|
|
103
|
-
```text
|
|
104
|
-
Equality: { field: value }
|
|
105
|
-
|
|
106
|
-
Inequality: { field: { $ne: value } }
|
|
107
|
-
|
|
108
|
-
Null checks: { field: { $isNull: true | false } }
|
|
109
|
-
|
|
110
|
-
Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
|
|
111
|
-
|
|
112
|
-
Sets: { field: { $in: [v1, v2] } }
|
|
113
|
-
|
|
114
|
-
Substring: { field: { $like: 'Get%' } } // case-sensitive
|
|
115
|
-
{ field: { $ilike: '%get%' } } // case-insensitive
|
|
116
|
-
|
|
117
|
-
Logic: and: [ {...}, {...} ]
|
|
118
|
-
or: [ {...}, {...} ]
|
|
119
|
-
|
|
120
|
-
Nested fields: 'relation.field': value
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
CRITICAL: The operator map above is the full set of `where` filters Instant
|
|
124
|
-
supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
|
|
125
|
-
`$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
|
|
126
|
-
|
|
127
|
-
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.
|
|
128
|
-
|
|
129
|
-
CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
|
|
130
|
-
|
|
131
|
-
# CRITICAL Permission Guidelines
|
|
132
|
-
|
|
133
|
-
Below are some CRITICAL guidelines for writing permissions in InstantDB.
|
|
134
|
-
|
|
135
|
-
## `data.ref`
|
|
136
|
-
|
|
137
|
-
- Use `data.ref("<path.to.attr>")` for linked attributes.
|
|
138
|
-
- Always returns a **list**.
|
|
139
|
-
- Must end with an **attribute**.
|
|
140
|
-
|
|
141
|
-
**Correct**
|
|
142
|
-
|
|
143
|
-
```cel
|
|
144
|
-
auth.id in data.ref('post.author.id') // auth.id in list of author ids
|
|
145
|
-
data.ref('owner.id') == [] // there is no owner
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Errors**
|
|
149
|
-
|
|
150
|
-
```cel
|
|
151
|
-
auth.id in data.post.author.id
|
|
152
|
-
auth.id in data.ref('author')
|
|
153
|
-
data.ref('admins.id') == auth.id
|
|
154
|
-
auth.id == data.ref('owner.id')
|
|
155
|
-
data.ref('owner.id') == null
|
|
156
|
-
data.ref('owner.id').length > 0
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## `auth.ref`
|
|
160
|
-
|
|
161
|
-
- Same as `data.ref` but path must start with `$user`.
|
|
162
|
-
- Returns a list.
|
|
163
|
-
|
|
164
|
-
**Correct**
|
|
165
|
-
|
|
166
|
-
```cel
|
|
167
|
-
'admin' in auth.ref('$user.role.type')
|
|
168
|
-
auth.ref('$user.role.type')[0] == 'admin'
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Errors**
|
|
172
|
-
|
|
173
|
-
```cel
|
|
174
|
-
auth.ref('role.type')
|
|
175
|
-
auth.ref('$user.role.type') == 'admin'
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Unsupported
|
|
179
|
-
|
|
180
|
-
```cel
|
|
181
|
-
newData.ref('x')
|
|
182
|
-
data.ref(someVar + '.members.id')
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## $users Permissions
|
|
186
|
-
|
|
187
|
-
- Default `view` permission is `auth.id == data.id`
|
|
188
|
-
- Default `update` and `delete` permissions is false
|
|
189
|
-
- Default `create` permission is true (anyone can sign up)
|
|
190
|
-
- Can override `view`, `update`, and `create`
|
|
191
|
-
- Cannot override `delete`
|
|
192
|
-
- The `create` rule runs during auth signup flows (not via `transact`). Use it to restrict signups or validate `extraFields`.
|
|
193
|
-
- `extraFields` require an explicit `create` rule. Without one, signup is blocked to prevent unvalidated writes.
|
|
194
|
-
|
|
195
|
-
## $files Permissions
|
|
196
|
-
|
|
197
|
-
- Default permissions are all false. Override as needed to allow access.
|
|
198
|
-
- `data.ref` does not work for `$files` permissions.
|
|
199
|
-
- Use `data.path.startsWith(...)` or `data.path.endsWith(...)` to write
|
|
200
|
-
path-based rules.
|
|
201
|
-
|
|
202
|
-
## Field-level Permissions
|
|
203
|
-
|
|
204
|
-
Restrict access to specific fields while keeping the entity public:
|
|
205
|
-
|
|
206
|
-
```json
|
|
207
|
-
{
|
|
208
|
-
"$users": {
|
|
209
|
-
"allow": {
|
|
210
|
-
"view": "true"
|
|
211
|
-
},
|
|
212
|
-
"fields": {
|
|
213
|
-
"email": "auth.id == data.id"
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Notes:
|
|
220
|
-
|
|
221
|
-
- Field rules override entity-level `view` for that field
|
|
222
|
-
- Useful for hiding sensitive data (emails, phone numbers) on public entities
|
|
223
|
-
|
|
224
|
-
# CRITICAL Storage Guidelines
|
|
225
|
-
|
|
226
|
-
CRITICAL: If an app displays images or files, use Instant Storage. Do not store
|
|
227
|
-
URLs as string attributes on your entities. This includes seed scripts: do not
|
|
228
|
-
use placeholder image URLs (e.g. picsum.photos) as string attributes to fake
|
|
229
|
-
file support.
|
|
230
|
-
|
|
231
|
-
Uploads auto-create `$files` entities. Link them to your data via the schema,
|
|
232
|
-
then query through the relationship to get URLs.
|
|
233
|
-
|
|
234
|
-
CRITICAL: You MUST include `$files` in your schema entities if you use Storage.
|
|
235
|
-
|
|
236
|
-
CRITICAL: `$files` entities can only be created via `db.storage.uploadFile`. You
|
|
237
|
-
cannot create `$files` via `db.transact`, and you cannot set `url` via transactions.
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
entities: {
|
|
241
|
-
$files: i.entity({
|
|
242
|
-
path: i.string().unique().indexed(),
|
|
243
|
-
url: i.string(),
|
|
244
|
-
}),
|
|
245
|
-
posts: i.entity({
|
|
246
|
-
caption: i.string(),
|
|
247
|
-
}),
|
|
248
|
-
},
|
|
249
|
-
links: {
|
|
250
|
-
postImage: {
|
|
251
|
-
forward: { on: "posts", has: "one", label: "image" },
|
|
252
|
-
reverse: { on: "$files", has: "many", label: "posts" },
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Upload and link the returned file ID to your entity
|
|
257
|
-
const postId = id();
|
|
258
|
-
const { data } = await db.storage.uploadFile(`posts/${postId}/${file.name}`, file);
|
|
259
|
-
db.transact(
|
|
260
|
-
db.tx.posts[postId].update({ caption }).link({ image: data.id })
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// Query through the relationship to get the URL
|
|
264
|
-
const { data } = db.useQuery({ posts: { image: {} } });
|
|
265
|
-
<img src={post.image.url} />
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
# CRITICAL Rooms Guidelines
|
|
269
|
-
|
|
270
|
-
CRITICAL: Hooks for presence and topics live on `db.rooms` and take the room as the first arg. The room object itself has no `usePresence` or `publishPresence` methods.
|
|
271
|
-
|
|
272
|
-
Rooms host two ephemeral primitives: presence (cursor positions, who's online) and topics (live reactions). Use them only for data that should NOT persist. Persisted data via `transact` already syncs in real-time to all subscribed clients, so reach for rooms only when the data is intentionally ephemeral.
|
|
273
|
-
|
|
274
|
-
## Presence
|
|
275
|
-
|
|
276
|
-
Each peer publishes a presence object readable by all other peers in the room. Retained for the connection and cleaned up automatically on disconnect.
|
|
277
|
-
|
|
278
|
-
```tsx
|
|
279
|
-
const room = db.room('chat', 'main');
|
|
280
|
-
const { user, peers, publishPresence } = db.rooms.usePresence(room, {
|
|
281
|
-
initialPresence: { x: 0, y: 0 },
|
|
282
|
-
});
|
|
283
|
-
// peers is keyed by peerId, not an array. Use Object.values(peers) to iterate
|
|
284
|
-
publishPresence({ x: 50, y: 50 });
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
## Topics
|
|
288
|
-
|
|
289
|
-
Topic payloads aren't retained. Peers only see events fired while they're listening.
|
|
290
|
-
|
|
291
|
-
```tsx
|
|
292
|
-
const room = db.room('chat', 'main');
|
|
293
|
-
|
|
294
|
-
const publishEmoji = db.rooms.usePublishTopic(room, 'emoji');
|
|
295
|
-
publishEmoji({ name: 'fire' });
|
|
296
|
-
|
|
297
|
-
db.rooms.useTopicEffect(room, 'emoji', (payload) => {
|
|
298
|
-
animateEmoji(payload.name);
|
|
299
|
-
});
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
# Best Practices
|
|
303
|
-
|
|
304
|
-
## Pass `schema` when initializing Instant
|
|
305
|
-
|
|
306
|
-
Always pass `schema` when initializing Instant to get type safety for queries and transactions
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
import schema from '@/instant.schema';
|
|
310
|
-
|
|
311
|
-
// On client
|
|
312
|
-
import { init } from '@instantdb/react'; // or your relevant Instant SDK
|
|
313
|
-
const clientDb = init({ appId, schema });
|
|
314
|
-
|
|
315
|
-
// On backend
|
|
316
|
-
import { init } from '@instantdb/admin';
|
|
317
|
-
const adminDb = init({ appId, adminToken, schema });
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## Use `id()` to generate ids
|
|
321
|
-
|
|
322
|
-
Always use `id()` to generate ids for new entities
|
|
323
|
-
|
|
324
|
-
```tsx
|
|
325
|
-
import { id } from '@instantdb/react'; // or your relevant Instant SDK
|
|
326
|
-
import { clientDb } from '@/lib/clientDb';
|
|
327
|
-
clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## Use Instant utility types for data models
|
|
331
|
-
|
|
332
|
-
Always use Instant utility types to type data models
|
|
333
|
-
|
|
334
|
-
```tsx
|
|
335
|
-
import { AppSchema } from '@/instant.schema';
|
|
336
|
-
|
|
337
|
-
type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
|
|
338
|
-
type PostsWithProfile = InstaQLEntity<
|
|
339
|
-
AppSchema,
|
|
340
|
-
'posts',
|
|
341
|
-
{ author: { avatar: {} } }
|
|
342
|
-
>; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## Use `db.useAuth` or `db.subscribeAuth` for auth state
|
|
346
|
-
|
|
347
|
-
```tsx
|
|
348
|
-
import { clientDb } from '@/lib/clientDb';
|
|
349
|
-
|
|
350
|
-
// For react/react-native apps use db.useAuth
|
|
351
|
-
function App() {
|
|
352
|
-
const { isLoading, user, error } = clientDb.useAuth();
|
|
353
|
-
if (isLoading) { return null; }
|
|
354
|
-
if (error) { return <Error message={error.message} />; }
|
|
355
|
-
if (user) { return <Main />; }
|
|
356
|
-
return <Login />;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// For vanilla JS apps use db.subscribeAuth
|
|
360
|
-
function App() {
|
|
361
|
-
renderLoading();
|
|
362
|
-
db.subscribeAuth((auth) => {
|
|
363
|
-
if (auth.error) { renderAuthError(auth.error.message); }
|
|
364
|
-
else if (auth.user) { renderLoggedInPage(auth.user); }
|
|
365
|
-
else { renderSignInPage(); }
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
## Set custom properties at signup with `extraFields`
|
|
371
|
-
|
|
372
|
-
Pass `extraFields` to any sign-in method to write custom `$users` properties atomically on user creation.
|
|
373
|
-
Fields must be defined as optional attrs on `$users` in your schema.
|
|
374
|
-
Use the `created` boolean to scaffold data for new users.
|
|
375
|
-
|
|
376
|
-
```tsx
|
|
377
|
-
// Set properties at signup
|
|
378
|
-
const { user, created } = await db.auth.signInWithMagicCode({
|
|
379
|
-
email,
|
|
380
|
-
code,
|
|
381
|
-
extraFields: { nickname, createdAt: Date.now() },
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Scaffold data for new users
|
|
385
|
-
if (created) {
|
|
386
|
-
db.transact([
|
|
387
|
-
db.tx.settings[id()]
|
|
388
|
-
.update({ theme: 'light', notifications: true })
|
|
389
|
-
.link({ user: user.id }),
|
|
390
|
-
]);
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
# Ad-hoc queries from the CLI
|
|
395
|
-
|
|
396
|
-
Run `npx instant-cli query '{ posts: {} }' --admin` to query your app. A context flag is required: `--admin`, `--as-email <email>`, or `--as-guest`. Also supports `--app <id>`.
|
|
397
|
-
|
|
398
|
-
# Instant Documentation
|
|
399
|
-
|
|
400
|
-
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
|
|
401
|
-
|
|
402
|
-
- [TOPIC](URL): Description of the topic.
|
|
403
|
-
|
|
404
|
-
Fetch the URL for a topic to learn more about it.
|
|
405
|
-
|
|
406
|
-
- [Common mistakes](https://www.instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
|
|
407
|
-
- [Initializing Instant](https://www.instantdb.com/docs/init.md): How to integrate Instant with your app.
|
|
408
|
-
- [Modeling data](https://www.instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
|
|
409
|
-
- [Writing data](https://www.instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
|
|
410
|
-
- [Reading data](https://www.instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
|
|
411
|
-
- [Instant on the Backend](https://www.instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
|
|
412
|
-
- [Patterns](https://www.instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
|
|
413
|
-
- [Auth](https://www.instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
|
|
414
|
-
- [Guest Auth](https://www.instantdb.com/docs/auth/guest-auth.md): How to add guest auth to your Instant app.
|
|
415
|
-
- [Other Auth](https://www.instantdb.com/docs/auth.md): Additional auth methods supported by Instant.
|
|
416
|
-
- [Managing users](https://www.instantdb.com/docs/users.md): How to manage users in your Instant app.
|
|
417
|
-
- [Presence, Cursors, and Activity](https://www.instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
|
|
418
|
-
- [Instant CLI](https://www.instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
|
|
419
|
-
- [Storage](https://www.instantdb.com/docs/storage.md): How to upload and serve files with Instant.
|
|
420
|
-
- [Streams](https://www.instantdb.com/docs/streams.md): How to use streams with Instant.
|
|
421
|
-
- [Stripe Payments](https://www.instantdb.com/docs/stripe-payments.md): How to integrate Stripe payments with Instant.
|
|
422
|
-
- [React Native](https://www.instantdb.com/docs/start-rn.md): How to use Instant in React Native apps.
|
|
423
|
-
- [Vanilla JS](https://www.instantdb.com/docs/start-vanilla.md): How to use Instant in vanilla JS apps.
|
|
424
|
-
- [SolidJS](https://www.instantdb.com/docs/start-solidjs.md): How to use Instant in SolidJS apps.
|
|
425
|
-
- [Svelte](https://www.instantdb.com/docs/start-svelte.md): How to use Instant in Svelte apps.
|
|
426
|
-
- [Vue](https://www.instantdb.com/docs/start-vue.md): How to use Instant in Vue apps.
|
|
427
|
-
- [TanStack](https://www.instantdb.com/docs/start-tanstack.md): How to use Instant in TanStack apps.
|
|
428
|
-
|
|
429
|
-
# Final Note
|
|
430
|
-
|
|
431
|
-
Think before you answer. Make sure your code passes typechecks `tsc --noEmit` and works as expected.
|
|
432
|
-
Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!
|