In this guide, you will:

  1. Create a new local-first project using create-lofi-app
  2. Create a simple To-Do UI
  3. Connect to a Basic project and set up your schema
  4. Add sign in and sign out functionality
  5. Hook up to Basic DB so that your users can save their to-dos across their devices

Create a new local-first project

1

Create a new local-first project

Terminal
# create a new local-first project:
npx create-lofi-app

When prompted in the CLI, enter y for installing the vite package. Then give your project a name. And finally choose the Basic + Tailwind variant.

2

install npm packages

cd project-name into your project, run npm i, and start your server with npm run dev:

Terminal
# first, cd into your project
cd project-name

# then, install all packages
npm i

# start your server
npm run dev

You’ll now be able to navigate to http://localhost:5173 and see the app running as below:

Create a simple To-Do UI

We’ll replace the existing homepage UI with a simple To-Do app UI using Tailwind that comes preinstalled with the create-lofi-app template.

1

Delete the existing homepagecode

Let’s replace the code in your src/App.tsx file with the following for a clean slate:

import './App.css'

function App() {
  return (
    <>
      <h1>my lofi to-do app</h1>
    </>
  )
}

export default App
2

Add an input + button

Let’s add an input, and a button to the app, and add some basic Tailwind styling

App.tsx
import './App.css'

function App() {
  return (
    <div className="flex flex-col items-center">
      <h1 className="mb-4">my lofi to-do app</h1>
      <div className="flex gap-2">
        <input
          type="text"
          placeholder="Add a to-do"
          className="border rounded px-2 py-1"
        />
        <button className="bg-blue-500 px-3 py-1 rounded">
          Add
        </button>
      </div>
    </div>
  )
}

export default App

You should now see a simple input and button on the page:

Connect to a Basic project and set up your schema

The fastest way to connect to a Basic project is to use the Basic CLI.

1

Install the Basic CLI

Make sure you are in the directory of your project.

Terminal
# install the basic cli
npm install -g @basictech/cli

# create a Basic account and login
basic login

You’ll be redirected in your browser to sign up for a Basic account and login. Once you’ve logged in, you’ll be able to connect to a Basic project.

2

Connect to a Basic project

Terminal
# connect to a Basic project
basic init

Choose to Create new project

You’ll then be prompted to give your Basic project a name (this doesn’t have to match your directory name, but it’s best to keep it consistent), and to select a project template.

Choose the typescript and hit Yep!

3

Set up your schema

If you navigate to the src/basic.config.ts file, you’ll see that it’s already set up for you now with a schema object with a project_id!

Let’s change the schema to match the needs of our To-Do app, and add a name string field and a completed boolean field.

basic.config.ts
export const schema = {
  project_id: "your-project-id",
  version: 0,
  tables: {
    todos: {
      type: "collection",
      fields: {
        name: {
          type: "string"
        },
        completed: {
          type: "boolean"
        }
      }
    }
  }
}
4

Push the changes to our schema

Terminal
# push the changes to our schema
basic push

Add sign in and sign out functionality

It’s very easy to add the ability to have your users sign in and sign out of your to-do app.

1

Add a login and logout button to App.tsx

Now navigate to src/App.tsx and import signin, signout, isSignedIn, and user.

App.tsx
/// add back the UseBasic import
import { useBasic } from '@basictech/react'
import './App.css'

function App() {
  // Import signin to enable the user to sign in
  // Import signout to enable the user to sign out
  // Import isSignedIn to check if the user is signed in
  // Import user to get the user's information once they're signed in
  const { signin, isSignedIn, user, signout } = useBasic()

  return (
    <div className="flex flex-col items-center">
      <h1 className="mb-4">my lofi to-do app</h1>
      <div className="flex gap-2">
        <input
          type="text"
          placeholder="Add a to-do"
          className="border rounded px-2 py-1"
        />
        <button className="bg-blue-500 px-3 py-1 rounded">
          Add
        </button>
      </div>

      {/* if not signed in yet, show the sign in button */}
      {/* when signed in, show the user's email */}
      {isSignedIn ? (
        <div className="mt-12">
          <p>Signed in as: {user?.email}</p>
          {/* Add a button to sign out */}
          <button" onClick={signout}>Sign Out</button>
        </div>
      ) : (
        <button className="mt-12" onClick={signin}>Sign In</button>
      )}
    </>
  )
}

export default App

All of it put together should look like this when signed in:

Hook up to Basic DB

Now that we have the ability to sign in and sign out, we can hook up to Basic DB so that our users can save their to-dos across their devices.

1

Submit a task to Basic DB

We will add some state to capture our input value, implement the .add() method, and then console.log the result.

App.tsx
import { useBasic } from '@basictech/react'
// import useState
import { useState } from 'react'
import './App.css'

function App() {
  // add the db import
  const { signin, signout, isSignedIn, user, db } = useBasic()

  // add the taskInput state
  const [taskInput, setTaskInput] = useState('')

  // add the addTask function
  const addTask = () => {
    if (!taskInput.trim()) return // only runs if the taskInput is not empty

    db.collection('todos').add({
      name: taskInput,
      completed: false
    })
      .then((item: any) => {
        setTaskInput('') // Clear input after successful add
        console.log("Successfully added new todo with ID: ", item)
      })
      .catch((error: any) => {
        console.error("Error adding todo: ", error)
      })
  }

  return (
    <div className="flex flex-col items-center">
      <h1 className="mb-4">my lofi to-do app</h1>
      <div className="flex gap-2">
        <input
          type="text"
          placeholder="Add a to-do"
          className="border rounded px-2 py-1"
          // add the value and onChange states so we can capture the input
          value={taskInput}
          onChange={(e) => setTaskInput(e.target.value)}
        />

        {/* add the addTask function to the button */}
        <button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
          Add
        </button>
      </div>

      {isSignedIn ? (
        <div className="mt-12">
          <p>Signed in as: {user?.email}</p>
          <button onClick={signout}>Sign Out</button>
        </div>
      ) : (
        <button className="mt-12" onClick={signin}>Sign In</button>
      )}
    </div>
  )
}

export default App
2

Read and display the tasks

Now let’s retrieve and display the tasks!

App.tsx
/ add useQuery hook import
import { useBasic, useQuery } from '@basictech/react'
import { useState } from 'react'
import './App.css'

function App() {
  const { signin, signout, isSignedIn, user, db } = useBasic()
  const [taskInput, setTaskInput] = useState('')

  // get all tasks from the database
  const tasks = useQuery(() => db.collection('todos').getAll())

  const addTask = () => {
    if (!taskInput.trim()) return

    db.collection('todos').add({
      name: taskInput,
      completed: false
    })
      .then((item: any) => {
        setTaskInput('')
        console.log("Successfully added new todo with ID: ", item)
      })
      .catch((error: any) => {
        console.error("Error adding todo: ", error)
      })
  }

  return (
    <div className="flex flex-col items-center">
      <h1 className="mb-4">my lofi to-do app</h1>
      <div className="flex gap-2">
        <input
          type="text"
          placeholder="Add a to-do"
          className="border rounded px-2 py-1"
          value={taskInput}
          onChange={(e) => setTaskInput(e.target.value)}
        />

        <button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
          Add
        </button>
      </div>

      {/* Add the tasks list here */}
      <div className="mt-4 w-full max-w-md">
        {tasks?.map((task: any) => (
          <div
            key={task.id}
            className="flex items-center p-2"
          >
            <span>{task.name}</span>
          </div>
        ))}
      </div>

      {isSignedIn ? (
        <div className="mt-12">
          <p>Signed in as: {user?.email}</p>
          <button onClick={signout}>Sign Out</button>
        </div>
      ) : (
        <button className="mt-12" onClick={signin}>Sign In</button>
      )}
    </div>
  )
}

export default App

With just a few lines you were able to display your tasks as such:

3

Add a delete button

Now let’s add a delete button to our tasks using the .delete() method.

App.tsx
import { useBasic, useQuery } from '@basictech/react'
import { useState } from 'react'
import './App.css'

function App() {
  const { signin, signout, isSignedIn, user, db } = useBasic()
  const [taskInput, setTaskInput] = useState('')

  const tasks = useQuery(() => db.collection('todos').getAll())

  const addTask = () => {
    if (!taskInput.trim()) return

    db.collection('todos').add({
      name: taskInput,
      completed: false
    })
      .then((item: any) => {
        setTaskInput('')
        console.log("Successfully added new todo with ID: ", item)
      })
      .catch((error: any) => {
        console.error("Error adding todo: ", error)
      })
  }

  // add the deleteTask function
  const deleteTask = (id: string) => {
    db.collection('todos').delete(id)
  }

  return (
    <div className="flex flex-col items-center">
      <h1 className="mb-4">my lofi to-do app</h1>
      <div className="flex gap-2">
        <input
          type="text"
          placeholder="Add a to-do"
          className="border rounded px-2 py-1"
          value={taskInput}
          onChange={(e) => setTaskInput(e.target.value)}
        />

        <button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
          Add
        </button>
      </div>

      {/* update styling to add a delete button */}
      <div className="mt-4 w-full max-w-md">
        {tasks?.map((task: any) => (
          <div
            key={task.id}
            className="flex items-center justify-between p-2 border-b"
          >
            <span>{task.name}</span>
            <button
              onClick={() => deleteTask(task.id)}
              className="text-red-500 hover:text-red-700"
            >
              Delete
            </button>
          </div>
        ))}
      </div>

      {isSignedIn ? (
        <div className="mt-12">
          <p>Signed in as: {user?.email}</p>
          <button onClick={signout}>Sign Out</button>
        </div>
      ) : (
        <button className="mt-12" onClick={signin}>Sign In</button>
      )}
    </div>
  )
}

export default App

The delete button would look like this:

And that’s it! You’ve now built a local-first to-do app that allows your users to sign in, sign out, and save their to-dos across their devices.

You’ll also notice how snappily all the changes are reflected on the app, from task creation to deletion - this is the power of local-first architecture!