Build and deploy a production-ready to-do app in under an hour with Userbase and Svelte
by Alexander Obenauer
in
Learn to Code
published
April 14, 2020
by Alexander Obenauer
in
Learn to Code
published
April 14, 2020

In this start-to-finish tutorial, we'll build a production-grade to-do app with Userbase, a new BaaS offering, and Svelte, a fantastic way to build web apps.

This article assumes you have some familiarity with web development, but little to no familiarity with Userbase or Svelte.

We'll start with some background on these technologies, and then dive into the process.

What is Userbase

Userbase just launched as a wild new backend-as-a-service offering which keeps user data encrypted end-to-end, and sports a dramatically simple API.

It allows you to build and ship simple HTML, CSS, and Javascript - an entirely static website - with production-grade user authentication and data storage, without having to worry about any of the backend.

What is Svelte

Svelte has been around for some time, and remains my favorite way to build front-end apps.

Imagine if you had a language where you could declare not just what a variable's value should be, but instead how it should be computed. What if that declaration then kept the value up-to-date at all times, and updated anything that depended on it at the same time. 

var values = []
store.subscribe((newValues) => values = newValues)
dynamic var count = values.length
...
<div id="count">{count}</div>

Imagine if our dynamic var is always updated automatically when values is updated. And imagine if our div's contents were then, also, automatically updated.

Wild, right?

Well, that's what Svelte is. Technically its own language that operates within the confines of Javascript syntax, along with the compiler that converts that language into plain Javascript which automatically keeps everything in your app up-to-date as state changes occur.

It's the best of both worlds: a declarative syntax like we enjoy in React, but with the efficiency of no framework (not only do Svelte apps run faster, users don't have to unwittingly download massive amounts of Javascript. Imagine using a UI library in Svelte; it's a dev dependency! Only what you actually use ever gets included in what is sent to users' browsers).

Let's start building

Seriously, this should take less than an hour. Here we go.

Step 1: Create a Userbase account

Head to Userbase.com, click "Try it free", and set up a free account.

It'll start you off with a trial app. Keep this window open, we'll use the "App ID" soon.

Step 2: Create a Svelte project

Install npm on your machine if you haven't before.

In your terminal, run this command to prepare and run a starter Svelte app:

npx degit sveltejs/template TodoApp
cd TodoApp
npm install
npm run dev

That last line, npm run dev is what you call from the TodoApp directory whenever you want to run your app for development purposes.

Open http://localhost:5000 to see the starter Svelte app running.

Step 3: Build Userbase access into a Svelte store

Svelte has a fantastic stores feature that we'll use here to interact with our user's accounts and data in Userbase.

If you've got Atom installed, you can run atom ./ in a new terminal tab to open the code editor loaded with the TodoApp project files.

3.1 Include the Userbase JS SDK

Open TodoApp/public/index.html, and include Userbase's JS SDK above the bundle.js line:

<script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>

The file should now look like this:

TodoApp/public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>

    <title>Svelte app</title>

    <link rel='icon' type='image/png' href='/favicon.png'>
    <link rel='stylesheet' href='/global.css'>
    <link rel='stylesheet' href='/build/bundle.css'>

    <script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>
    <script defer src='/build/bundle.js'></script>
</head>

<body>
</body>
</html>

3.2 Create the Svelte stores for accessing Userbase

Inside TodoApp/src, a directory already made for you, create a new directory called Stores. We're going to create two Svelte stores inside that directory.

Create the UserAccount store

Inside TodoApp/src/Stores, create a new file called UserAccount.js. Paste the following into it:

TodoApp/src/Stores/UserAccount.js

import { writable } from 'svelte/store';

/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure

const state = {
  initialized: false,
  signedIn: false,
  username: undefined,
  error: undefined
}

/** ACTION CREATORS **/
/// These are the only things exposed to the outside world

const actionCreators = (store) => {
  return {
    initialize: () => {
      userbase.init({ appId: 'YOUR_APP_ID_HERE' })
            .then((session) => {
                if (session.user) {
                    store.dispatch(SignedIn(session.user.username))
                } else {
            store.dispatch(SignedOut())
                }
            })
            .catch((e) => store.dispatch(Error(e)))
            .finally(() => store.dispatch(Initialized()))
    },
    signUp: (username, password, rememberMe) => {
      userbase.signUp({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
        .then((user) => store.dispatch(SignedIn(username)))
        .catch((e) => store.dispatch(Error(e)))
    },
    signIn: (username, password, rememberMe) => {
      userbase.signIn({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
        .then((user) => store.dispatch(SignedIn(username)))
        .catch((e) => store.dispatch(Error(e)))
    },
    signOut: () => {
        userbase.signOut()
            .then(() => store.dispatch(SignedOut()))
            .catch((e) => store.dispatch(Error(e)))
    }
  }
}

/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.

const Initialized = () => ({ name: "Initialized" })
const SignedIn = (username) => ({ name: "SignedIn", username })
const SignedOut = () => ({ name: "SignedOut" })
const Error = (error) => ({ name: "Error", error })

/** REDUCER **/
/// Receives internal actions as sent by action creators

const reducer = (state, action) => {
  switch (action.name) {
    case "Initialized":
      return { ...state,
        initialized: true
      };

    case "SignedIn":
      return { ...state,
        username: action.username,
        signedIn: true,
        error: undefined
      };

    case "SignedOut":
      return { ...state,
        username: undefined,
        signedIn: false,
        error: undefined
      };

    case "Error":
      return { ...state,
        error: action.error
      }

    default:
      return state;
  }
};

/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer

class Store {
  constructor(subscribe, updateState, reducer) {
    this.updateState = updateState
    this.reducer = reducer

    this.unsubscribe = subscribe(newState => this.state = newState)
  }

  dispatch(action) {
    this.updateState(s => this.reducer(s, action))
  }
}

/** SVELTE STORE **/

function createUserAccountStore() {
    const { subscribe, update, set } = writable(state);
  const store = new Store(subscribe, update, reducer)
  const actions = actionCreators(store)

    return {
    ...actions,
        subscribe
    };
}

export const userAccountStore = createUserAccountStore();

Make sure to replace YOUR_APP_ID_HERE with the app ID from Userbase

This code produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores (we'll see all that in just a minute).

Internally, it updates its state with an architecture resembiling a Flux store for better maintainability.


3.3 Create the Todos store

Inside TodoApp/src/Stores, create a new file called Todos.js. Paste the following into it:

TodoApp/src/Stores/Todos.js

import { writable } from 'svelte/store';

/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure

const state = {
  initialized: false,
  todos: []
}

const todo = {
  title: '',
  complete: false
}

/** ACTION CREATORS **/
/// These are the only things exposed to the outside world

const actionCreators = (store) => {
  return {
    initialize: () => {
      userbase.openDatabase({
        databaseName: 'todos',
        changeHandler: (todos) => store.dispatch(TodosLoaded(todos))
      })
      .catch((e) => store.dispatch(Error(e)))
      .finally(() => store.dispatch(Initialized()))
    },
    createTodo: (item) => {
      item.title = item.title || ""

      userbase.insertItem({
        databaseName: 'todos',
        item
      })
      .catch((e) => store.dispatch(Error(e)))
    },

    updateTodo: (todo, item) => {
      userbase.updateItem({ databaseName: 'todos', itemId: todo.itemId, item: {
        ...todo.item,
        ...item
      }})
      .catch((e) => store.dispatch(Error(e)))
    },

    deleteTodo: (itemId) => {
      userbase.deleteItem({ databaseName: 'todos', itemId: itemId })
        .catch((e) => store.dispatch(Error(e)))
    }
  }
}

/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.

const Initialized = () => ({ name: "Initialized" })
const TodosLoaded = (todos) => ({ name: "TodosLoaded", todos })
const Error = (error) => ({ name: "Error", error })

/** REDUCER **/
/// Receives internal actions as sent by action creators

const reducer = (state, action) => {
  switch (action.name) {
    case "Initialized":
      return { ...state,
        initialized: true
      };

    case "TodosLoaded":
      return { ...state,
        todos: action.todos
      };

    case "Error":
      return { ...state,
        error: action.error
      }

    default:
      return state;
  }
};

/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer

class Store {
  constructor(subscribe, updateState, reducer) {
    this.updateState = updateState
    this.reducer = reducer

    this.unsubscribe = subscribe(newState => this.state = newState)
  }

  dispatch(action) {
    this.updateState(s => this.reducer(s, action))
  }
}

/** SVELTE STORE **/

function createTodosStore() {
    const { subscribe, update, set } = writable(state);
  const store = new Store(subscribe, update, reducer)
  const actions = actionCreators(store)

    return {
    ...actions,
        subscribe
    };
}

export const todosStore = createTodosStore();

This also produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores.

Just like the UserAccount store, it also updates its internal state with an architecture resembling a Flux store for better maintainability.


Step 4: Build the app interface

Since Userbase is taking care of all the backend needs of our app, we primarily only need to build the interface.

4.1 The fundamentals

Open TodoApp/src/App.svelte. This is where we are going to begin building our app's interface.

You can see this file already has some code in it. Let's go ahead and replace that with this:

TodoApp/src/App.svelte

<script>
    // Library imports
    import { onMount } from 'svelte';

    // Component imports
    import SignUp from './SignUp.svelte';
    import SignIn from './SignIn.svelte';
    import TodoList from './TodoList.svelte';

    // State
    import { userAccountStore } from './Stores/UserAccount.js';
    $: initialized = $userAccountStore.initialized
    $: signedIn = $userAccountStore.signedIn
    $: username = $userAccountStore.username
    $: error = $userAccountStore.error


    // Events

    onMount(() => {
        userAccountStore.initialize()
    })

    function signOut() {
        userAccountStore.signOut()
    }

</script>

<main>
    <h1 id="logo">TODO</h1>

    <div id="content">
        {#if initialized}
            {#if signedIn}
                <TodoList />
            {:else}
                <SignIn />
                <div class="divider"></div>
                <SignUp />
            {/if}
        {:else}
            Loading...
        {/if}

        {#if error}<div id="error">{error}</div>{/if}
    </div>

    {#if signedIn}
        <div id="logout">
            <button on:click={signOut}>Logout {username}</button>
        </div>
    {/if}
</main>

<style>
    main {
        text-align: center;
        padding: 50px 0;
    }

    #content {
        padding: 20px;
        max-width: 360px;
        margin: 0 auto;

        background: #FFFFFF;
        box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05), 0 12px 50px 0 rgba(0,0,0,0.15);
        border-radius: 12px;
    }

    #logout {
        margin-top: 20px;
    }

    h1 {
        color: #ff3e00;
        text-transform: uppercase;
        font-size: 4em;
        font-weight: 100;
        margin: 20px 0;
    }

    .divider {
        border-top: 1px dotted rgba(0,0,0,0.25);
        margin: 50px 0px;
    }
</style>

This is all pretty straightforward, but a few notes:

onMount is one of Svelte's lifecycle methods (more here). There are a number of them you can use to run code at specific points in the component's lifecycle.

$: is how you declare a variable which should automatically stay up to date (instead of var, let, or const). Note: there is even more than just declarations that $: can do, more on that here.

$anySvelteStore automatically subscribes to that store (and unsubscribes from it when the component is destroyed), providing its value wherever used.

So putting those two together:

$: username = $userAccountStore.username

This line will always ensure that username is up-to-date with the latest value from our UserAccount store to give us a currently signed-in user's username.

Finally, in Svelte, "HTML" can also contain other Svelte components, as well as variables from your <script> section.

So we can use that always-updated username value in our Svelte HTML:

<button on:click={signOut}>Logout {username}</button>

The button will always, automatically reflect the user's current username, even if it changes. Automatically!

Now we did use a number of other Svelte components which don't actually exist yet, so you'll see in your first terminal tab that the compiler is showing an error. Let's fix that by building out the rest of the app's interface.


4.2 The other components

Now let's build out our other components. We're going to create five custom Svelte components. For each, create the new file with the right file name and paste the contents into it.

TodoApp/src/SignIn.svelte

<script>
  import { userAccountStore } from './Stores/UserAccount.js';

  var username;
  var password;

  function handleSignIn(e) {
    e.preventDefault()

    userAccountStore.signIn(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
  }
</script>

<!-- HTML -->
<h1>Sign In</h1>
<form id="signin-form" on:submit={handleSignIn}>
  <input id="signin-username" type="text" required placeholder="Username" bind:value={username}>
  <input id="signin-password" type="password" required placeholder="Password" bind:value={password}>
  <input type="submit" value="Sign in">
</form>
{#if $userAccountStore.error}
  <div id="error">{$userAccountStore.error}</div>
{/if}

<style>
  #signin-form input {
    display: block;
    margin: 10px auto;
  }
</style>

TodoApp/src/SignUp.svelte

<script>
  import { userAccountStore } from './Stores/UserAccount.js';

  var username;
  var password;

  function handleSignUp(e) {
    e.preventDefault()

    userAccountStore.signUp(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
  }
</script>

<!-- HTML -->
<h1>Create an account</h1>
<form id="signup-form" on:submit={handleSignUp}>
  <input id="signup-username" type="text" required placeholder="Username" bind:value={username}>
  <input id="signup-password" type="password" required placeholder="Password" bind:value={password}>
  <input type="submit" value="Create an account">
</form>
{#if $userAccountStore.error}
  <div id="error">{$userAccountStore.error}</div>
{/if}

<style>
  #signup-form input {
    display: block;
    margin: 10px auto;
  }
</style>

TodoApp/src/TodoList.svelte

<script>
  // Library imports
  import { onMount } from 'svelte';

  // Component imports
  import TodoRow from './TodoRow.svelte';

  // State
  import { todosStore } from './Stores/Todos.js';
  $: initialized = $todosStore.todos
  $: error = $todosStore.error
  $: todos = $todosStore.todos || []

  let newTodoTitleValue;

  // Events

  onMount(() => {
    todosStore.initialize()
  })

  function addNewTodo(e) {
    e.preventDefault();

    todosStore.createTodo({
      title: newTodoTitleValue
    })

    newTodoTitleValue = ""
  }

</script>


<!-- HTML -->

<div id="todoList">
  {#each todos as todo, index (todo.itemId)}
    <TodoRow todo={todo} />
  {/each}

  <form id="addTodo" on:submit={addNewTodo}>
    <input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>
    <input type="submit" value="Add">
  </form>
</div>

{#if !initialized}
  <div id="loading">Loading to-dos...</div>
{/if}

{#if error}
  <div id="error">{error}</div>
{/if}

<style>
  #todoList {
    text-align: left;
    font-size: 14px;
  }

  form#addTodo {
    margin-top: 20px;
    display: flex;
  }

  form#addTodo #addTodoInput {
    flex-grow: 1;
    margin-right: 5px;
  }
</style>

TodoApp/src/TodoRow.svelte

<script>
  // Props
  export let todo;

  // Library imports
  import { afterUpdate } from 'svelte';

  // Component imports
  import Checkbox from './Checkbox.svelte';

  // Dynamic state
  import { todosStore } from './Stores/Todos.js';

  $: title = todo.item.title
  $: complete = todo.item.complete

  // Events

  function toggledCheckbox(e, todo) {
    e.preventDefault()
    todosStore.updateTodo(todo, { complete: !todo.item.complete })
  }
</script>

<!-- HTML -->
<div class={complete ? "todo complete" : "todo"} on:click>
  <Checkbox value={complete ? "checked" : "unchecked"} on:click={(e) => {toggledCheckbox(e, todo)}} />
  <p>{title}</p>
</div>

<style>
  .complete {
    opacity: 0.5;
  }

  .todo p {
    padding: 6px 0;
    padding-left: 22px;
    margin: 0;

    font-size: 16px;
    line-height: 20px;
  }
</style>

TodoApp/src/Checkbox.svelte

<script>
  // Props
  export let value;
</script>

<!-- HTML -->
{#if value == "none"}
  <div class="noCheckbox"></div>
{:else}
  <div class="container" on:click>
    {#if value == "checked"}<input type="checkbox" checked="checked">{:else}<input type="checkbox">{/if}
    <span class="checkmark"></span>
  </div>
{/if}

<style>
.noCheckbox {
  min-width: 22px;
  flex-grow: 0;
}

.container {
  min-width: 22px;
  flex-grow: 0;

  display: block;
  position: relative;
}

.container input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
  height: 0;
  width: 0;
}

.checkmark {
  border-radius: 3px;
  position: absolute;
  top: 8px;
  left: 0;
  height: 14px;
  width: 14px;
  border: 1px solid rgba(142,161,174,0.5);
  transition: all 0.2s ease;
}

.container:hover input ~ .checkmark {
  border: 1px solid rgba(142,161,174,1);
}

.container input:checked ~ .checkmark {
  border: 1px solid transparent;
  background-color: #2196F3;
}

.checkmark:after {
  content: "";
  position: absolute;
  display: none;
}

.container input:checked ~ .checkmark:after {
  display: block;
}

.container .checkmark:after {
  left: 5px;
  top: 3px;
  width: 2px;
  height: 5px;
  border: solid white;
  border-width: 0 2px 2px 0;
  -webkit-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}
</style>

A few notes on all of this code:

SignIn and SignUp repeat the same CSS. This is because Svelte keeps CSS from affecting any components outside of the one it is declared within. This helps you avoid all kinds of odd issues that can come up from the cascading part of CSS.

To avoid repeating CSS that you actually want to be global, put your global styles in /public/global.css.

But better yet, since the SignIn and SignUp components are so similar, you may want to simply combine them into one component, depending on how you would want your app's user authentication interface to work.

You can see our use of props which we pass to our custom components TodoRow and Checkbox. This is how state moves down into children components.

You can see our use of bindings in TodoList.svelte:

<input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>

By putting in bind:value={newTodoTitleValue}, we are creating a two-way binding with the variable newTodoTitleValue. This is how we get state into a parent component from a child component, and how we might update that child component with a new value. You can see both of these things happening in the addNewTodo function, where we first use the value of newTodoTitleValue, which is what the user has typed in, and then we set the value of newTodoTitleValue to an empty string, which clears the input field.

Finally, Checkbox dispatches its own event by forwarding the on:click event to one of its divs. For how to make custom component events, see more here.

Other than that, everything else largely follows the conventions we've already discussed.


4.3 Run your app

With that, your app should compile and run! If the compiler doesn't automatically attempt to recompile, hit CTRL+C in terminal, and npm run dev again.

Once it has compiled, you can load up your app at http://localhost:5000/, create an account, log out, sign back in, create tasks, and mark them as complete. If you refresh your browser window, you'll see that everything is persisted in your account, and if you open a second browser window and add or complete tasks, you'll see the first browser window automatically stay up to date.

This magic is all available thanks to Userbase's brilliant design and simple SDK, hooked into a lot of Svelte's magic which makes this starter app a fantastic foundation which can scale into a significantly more complex app without significantly more complex code.


Next steps

For a deeper understanding of Svelte, run through their fantastic tutorial at https://svelte.dev/tutorial/basics.

For a deeper understanding of Userbase, run through their Quickstart or read their docs at https://userbase.com/docs/.

With just these two resources, you can scale up to a very sophisticated production-ready app in very little time.

Happy creating!

Youll never look at email the same way again.
Eight years ago, we first reimagined email.
Now, we've done it again.
Thank you, we will be in touch soon!
Oops! Something went wrong while submitting the form.
“An ingenious new email service”
— David Pogue in the New York Times
“A good app, and well worth it.”
— Leo Laporte, This Week in Tech
“A joy to use”
— Bakari Chavanu, MakeUseOf
“Best designed email app on the App Store”
— Cam Bunton, Today’s iPhone
“Mail Pilot is a superb mail client”
— Dave Johnson, CBS MoneyWatch
“A clean, impressive, often beautiful way to manage unruly email”
— Nathan Alderman, Macworld