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 +8 -8
- package/package.json +3 -3
- package/template/rules/AGENTS.md +346 -0
- package/template/rules/cursor-rules.md +352 -0
- package/template/rules/windsurf-rules.md +352 -0
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
|
|
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
|
|
11
|
-
enable-pre-post-scripts=true`),e.base==="expo"){let n=
|
|
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}
|
|
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
|
|
15
|
-
`),n=!0;i.forEach(a=>{if(a.length===0){n?(t(""),n=!1):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,
|
|
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,
|
|
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.
|
|
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.
|
|
37
|
-
"instant-cli": "0.22.
|
|
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!
|