diff --git a/examples/typescript-supabase-todo/README.md b/examples/typescript-supabase-todo/README.md
new file mode 100644
index 00000000..b39dd4e5
--- /dev/null
+++ b/examples/typescript-supabase-todo/README.md
@@ -0,0 +1,20 @@
+# Supabase todo example
+
+Heavily adopted from `create-react-app` bootstrapping, here's a our Todo example with a Firebase backend bundled in a Typescript React application.
+
+## Live demo
+- https://homebase-example-ts-firebase-todo.vercel.app
+
+## Env vars
+REACT_APP_SUPABASE_URL
+supabaseAnonKey
+
+## Installation
+```
+yarn install
+```
+
+## Run it
+```
+yarn start
+```
diff --git a/examples/typescript-supabase-todo/package.json b/examples/typescript-supabase-todo/package.json
new file mode 100644
index 00000000..05d78bd6
--- /dev/null
+++ b/examples/typescript-supabase-todo/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "typescript-supabase-todo",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@supabase/supabase-js": "^1.15.1",
+ "@testing-library/jest-dom": "^5.11.4",
+ "@testing-library/react": "^11.1.0",
+ "@testing-library/user-event": "^12.1.10",
+ "@types/jest": "^26.0.15",
+ "@types/node": "^12.0.0",
+ "@types/react": "^16.9.53",
+ "@types/react-dom": "^16.9.8",
+ "firebase": "^8.1.1",
+ "firebaseui": "^4.7.1",
+ "homebase-react": "^0.7.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1",
+ "react-scripts": "4.0.0",
+ "typescript": "^4.0.3",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/typescript-supabase-todo/public/favicon.ico b/examples/typescript-supabase-todo/public/favicon.ico
new file mode 100644
index 00000000..a11777cc
Binary files /dev/null and b/examples/typescript-supabase-todo/public/favicon.ico differ
diff --git a/examples/typescript-supabase-todo/public/index.html b/examples/typescript-supabase-todo/public/index.html
new file mode 100644
index 00000000..6bd9b202
--- /dev/null
+++ b/examples/typescript-supabase-todo/public/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/typescript-supabase-todo/src/App.js b/examples/typescript-supabase-todo/src/App.js
new file mode 100644
index 00000000..7ddcd5ce
--- /dev/null
+++ b/examples/typescript-supabase-todo/src/App.js
@@ -0,0 +1,74 @@
+import { useState, useEffect } from 'react'
+import { supabase } from './supabaseClient'
+import Auth from './Auth'
+import { HomebaseProvider, useClient, useTransact, useQuery, useEntity, Transaction, Entity} from 'homebase-react'
+import Todos from './Todos'
+export default function App() {
+
+ const config = {
+ // Lookup helpers are used to enforce
+ // unique constraints and relationships.
+ lookupHelpers: {
+ user: { uid: { unique: 'identity' } },
+ todo: {
+ // refs are relationships
+ project: { type: 'ref' },
+ owner: { type: 'ref' }
+ }
+ },
+ // Initial data let's you conveniently transact some
+ // starting data on DB creation to hydrate your components.
+ initialData: [
+
+ ]
+ }
+
+ let [session, setSession] = useState(null)
+
+ const AuthChanged = (session) => {
+ const [transact] = useTransact()
+ const [currentUser] = useEntity({ identity: 'currentUser' })
+ const [client] = useClient()
+ setSession(session)
+ if (session){
+ const user = supabase.auth.user();
+ console.log(user)
+ transact([{ user: { uid: user.id, name: user.email} }])
+ client.transactSilently([{ currentUser: { identity: 'currentUser', uid: user.id }}])
+ }
+ }
+
+ useEffect(() => {
+ setSession(supabase.auth.session())
+ supabase.auth.onAuthStateChange((_event, session) => AuthChanged)
+ }, [])
+
+ return (
+
+
+ {!session ? (
+
+ ) : (
+
+
+ {
+ const { error } = await supabase.auth.signOut()
+ if (error) console.log('Error logging out:', error.message)
+ }}
+ >
+ Logout
+
+
+ )}
+
+
+
+ )
+}
diff --git a/examples/typescript-supabase-todo/src/Auth.js b/examples/typescript-supabase-todo/src/Auth.js
new file mode 100644
index 00000000..d79d1a97
--- /dev/null
+++ b/examples/typescript-supabase-todo/src/Auth.js
@@ -0,0 +1,138 @@
+import {useState} from 'react'
+import { supabase } from './supabaseClient'
+
+export default function Auth({}) {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+
+ const handleLogin = async (type, email, password) => {
+ try {
+ const { error , user} =
+ type === 'LOGIN'
+ ? await supabase.auth.signIn({ email, password })
+ : await supabase.auth.signUp({ email, password })
+ if (!error && !user) alert('Check your email for the login link!')
+ if (error) console.log('Error returned:', error.message)
+ } catch (error) {
+ console.log('Error thrown:', error.message)
+ alert(error.error_description || error)
+ }
+ }
+
+ async function handleOAuthLogin(provider) {
+ let { error } = await supabase.auth.signIn({ provider })
+ if (error) console.log('Error: ', error.message)
+ }
+
+ async function forgotPassword(e) {
+ e.preventDefault()
+ var email = prompt('Please enter your email:')
+ if (email === null || email === '') {
+ window.alert('You must enter your email.')
+ } else {
+ let { error } = await supabase.auth.api.resetPasswordForEmail(email)
+ if (error) {
+ console.log('Error: ', error.message)
+ } else {
+ alert('Password recovery email has been sent.')
+ }
+ }
+ }
+
+ return (
+
+
+
+ Email
+ setEmail(e.target.value)}
+ />
+
+
+ Password
+ setPassword(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ Or continue with
+
+
+
+
+
+
+ handleOAuthLogin('github')}
+ type="button"
+ className="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out"
+ >
+ GitHub
+
+
+
+
+
+ handleOAuthLogin('google')}
+ type="button"
+ className="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out"
+ >
+ Google
+
+
+
+
+
+
+
+ )
+}
diff --git a/examples/typescript-supabase-todo/src/Todos.tsx b/examples/typescript-supabase-todo/src/Todos.tsx
new file mode 100644
index 00000000..ce6bc1d8
--- /dev/null
+++ b/examples/typescript-supabase-todo/src/Todos.tsx
@@ -0,0 +1,282 @@
+import { HomebaseProvider, useClient, useTransact, useQuery, useEntity, Transaction, Entity} from 'homebase-react'
+/* eslint-disable react-hooks/exhaustive-deps */
+import React from 'react'
+import firebase from 'firebase/app'
+import 'firebase/auth'
+import 'firebase/database'
+import { supabase } from './supabaseClient'
+import Auth from './Auth';
+
+// this is a hack around firebaseui breaking ts support recently
+import * as firebaseui from 'firebaseui'
+import 'firebaseui/dist/firebaseui.css'
+export default function Todos(user) {
+ return(
+
+
+
+
+
+
+
)
+}
+
+declare const window: any;
+let subscription1 = null
+let subscription2 = null
+
+
+const DataSaver = (user) => {
+
+ const [client] = useClient()
+ window.client = client;
+ const [currentUser] = useEntity({ identity: 'currentUser' })
+ const userId = currentUser.get('uid')
+
+ const transactListener = React.useCallback((changedDatoms) => {
+ const numDatomChanges = changedDatoms.reduce((acc : any, [id, attr] : [number, string]) => (
+ {...acc, [id + attr]: (acc[id + attr] || 0) + 1}
+ ), {})
+ console.log(changedDatoms)
+ const datomsForFirebase = changedDatoms.filter(([id, attr, _, __, isAdded]: [number, any, any, any, boolean]) => !(!isAdded && numDatomChanges[id + attr] > 1))
+
+ datomsForFirebase.forEach(([id, attr, v, tx, isAdded]: [number, string, any, Transaction, boolean]) => {
+ console.log(isAdded);
+ if (isAdded){
+ supabase
+ .from('entities')
+ .insert({ "0": id, "1": attr, "2": v, "3":tx, "4": isAdded, "user_id": user.user.user.id })
+ .single()
+ .then(_ => console.log('works'))
+ .then( null, err => console.log('err: ', err));
+ } else {
+ supabase
+ .from('entities')
+ .delete()
+ .eq("E",id)
+ .then(_ => console.log('deleted'))
+ .then( null, err => console.log('err deleting: ', err));
+
+ }
+ })
+ }, [userId])
+ React.useEffect(() => {
+ //load in initial data
+ supabase.from('entities')
+ .select('0,1,2,3,4')
+ .order('id')
+ .then(function(res){
+ console.log(res);
+ if (!res.error){
+ console.log(res.data)
+ res.data.forEach(function(d){
+ let entity_name = d[1].split('/')[0].slice(1)
+ let attr = d[1].split('/')[1]
+ let transaction = [{[entity_name]: {id: d[0], [attr]: d[2]}} ]
+ client.transactSilently(transaction)
+ });
+ }
+ });
+ client.addTransactListener(transactListener)
+ const on = (action: any) => (ds: any) => client.transactSilently([[action, ...ds.val()]])
+ subscription1 = supabase
+ .from('entities')
+ .on('UPDATE', (v) => on('add'))
+ .on('INSERT', (v) => on('add'))
+ .on('DELETE', (v) => on('retract') )
+ .subscribe((change) => console.log('todos changed', change))
+
+ return () => {
+ client.removeTransactListener()
+ supabase.removeSubscription(subscription1)
+ }
+ }, [userId])
+ return null
+}
+
+const NewTodo = () => {
+ const [transact] = useTransact()
+ return (
+
+ )
+}
+
+const TodoList = () => {
+ const [filters] = useEntity({ identity: 'todoFilters' })
+ const [todos] = useQuery({
+ $find: 'todo',
+ $where: { todo: { name: '$any' } }
+ })
+ return (
+
+ {todos.filter(todo => {
+ if (!filters.get('showCompleted') && todo.get('isCompleted')) return false
+ if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) return false
+ if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false
+ return true
+ }).sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1)
+ .map(todo => )}
+
+ )
+}
+
+// PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity
+// this component stays disconnected from the useQuery in the parent TodoList.
+// useEntity creates a separate scope for every Todo so changes to TodoList
+// or sibling Todos don't trigger unnecessary re-renders.
+const Todo = React.memo(({ id } : {id : number}) => {
+ const [todo] = useEntity(id)
+ return (
+
+
+
+
+
+
+
+ ·
+
+ ·
+
+
+
+ {new Date(parseInt(todo.get('createdAt'))).toLocaleString()}
+
+
+ )
+})
+
+const TodoCheck = ({ todo } : {todo: Entity}) => {
+ const [transact] = useTransact()
+ return (
+ transact([{ todo: { id: todo.get('id'), isCompleted: e.target.checked } }])}
+ />
+ )
+}
+
+const TodoName = ({ todo } : {todo: Entity}) => {
+ const [transact] = useTransact()
+ return (
+ transact([{ todo: { id: todo.get('id'), name: e.target.value }}])}
+ />
+ )
+}
+
+const TodoProject = ({ todo } : {todo: Entity}) => {
+ const [transact] = useTransact()
+ return (
+ transact([{ todo: { id: todo.get('id'), project }}])}
+ />
+ )
+}
+
+const TodoOwner = ({ todo }: { todo: Entity}) => {
+ const [transact] = useTransact()
+ return (
+ transact([{ todo: { id: todo.get('id'), owner }}])}
+ />
+ )
+}
+
+const TodoDelete = ({ todo }: {todo: Entity}) => {
+ const [transact] = useTransact()
+ return (
+ transact([['retractEntity', todo.get('id')]])}>
+ Delete
+
+ )
+}
+
+const TodoFilters = () => {
+ const [filters] = useEntity({ identity: 'todoFilters' })
+ const [client] = useClient()
+ return (
+
+ Filter by:
+ Show Completed?
+ client.transactSilently([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked }}])}
+ />
+
+ ·
+ client.transactSilently([{ todoFilter: { id: filters.get('id'), project }}])}
+ />
+ ·
+ client.transactSilently([{ todoFilter: { id: filters.get('id'), owner }}])}
+ />
+
+ )
+}
+
+const EntitySelect = React.memo(({ label, entityType, value, onChange }:
+ {label: string, entityType: any, value: any, onChange:any}) => {
+ const [entities] = useQuery({
+ $find: entityType,
+ $where: { [entityType]: { name: '$any' } }
+ })
+ return (
+ {label}:
+ onChange && onChange(Number(e.target.value) || null)}
+ >
+
+ {entities.map(entity => (
+
+ {entity.get('name')}
+
+ ))}
+
+
+ )
+})
diff --git a/examples/typescript-supabase-todo/src/index.tsx b/examples/typescript-supabase-todo/src/index.tsx
new file mode 100644
index 00000000..c1f31c5f
--- /dev/null
+++ b/examples/typescript-supabase-todo/src/index.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+);
diff --git a/examples/typescript-supabase-todo/src/supabaseClient.ts b/examples/typescript-supabase-todo/src/supabaseClient.ts
new file mode 100644
index 00000000..04d09fe8
--- /dev/null
+++ b/examples/typescript-supabase-todo/src/supabaseClient.ts
@@ -0,0 +1,6 @@
+import { createClient } from '@supabase/supabase-js'
+
+const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
+const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey)