DEV Community

Fabio Giolito
Fabio Giolito

Posted on

Web3 with Svelte and Moralis

The goal of this app is to create a website where you can Sign In with an Ethereum Wallet using MetaMask, and create a profile. This should cover enough of how to use Svelte and Moralis so you can use as a starting point for your app.

Why Svelte (and not React)

Svelte is a Javascript framework for people who love HTML and CSS. React moves everything inside Javascript, making things unnecessarily complex, forcing you to re-learn how to do basic things. Svelte is much easier to understand, requires less code, and feels more like an extension of Javascript.

Why Moralis

Moralis is a Backend-as-a-service that also gives you simple APIs to work with multiple chains. Authentication, Database, Storage and Cloud functions are part of what you get out of the box. Having all that on the same place makes things faster to setup and develop.


Setting up the project

Let's jump straight into terminal and start a new SvelteKit project.

$ npm init svelte@next web3-starter
$ cd web3-starter
$ npx svelte-add tailwindcss
$ npm install
$ npm run dev -- --open
Enter fullscreen mode Exit fullscreen mode
  1. If you don't have NPM installed read this.
  2. Adding Tailwind is optional of course, but I enjoy using it. I won't focus on styles to keep things brief.

Adding Moralis

You'll need to create an account on Moralis, then create a server. Add your server details to a .env.development file on the root of your project.

VITE_PUBLIC_MORALIS_APP_ID="paste your app id here"
VITE_PUBLIC_MORALIS_SERVER_URL="paste your server url here"
Enter fullscreen mode Exit fullscreen mode

On terminal, stop the server with ctrl + c then restart it with $ npm run dev to pick up these variables.

Then add Moralis scripts to the head of your app.html.

  <!-- Moralis -->
  <script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
  <script src="https://unpkg.com/moralis/dist/moralis.js"></script>
Enter fullscreen mode Exit fullscreen mode

Now to configure Moralis, go to __layout.svelte. We'll add a function to get our .env variables and start Moralis, then call this function when the initial layout is Mounted (loaded), then load the rest of the app once Moralis has started.

<script>
  import "../app.css";
  import { onMount } from 'svelte';

  let moralisStarted = false; // track if started

  function configureMoralis() {
    // Get Env variables
    const serverUrl = import.meta.env.VITE_PUBLIC_MORALIS_SERVER_URL;
    const appId = import.meta.env.VITE_PUBLIC_MORALIS_APP_ID;
    // Start Moralis
    Moralis.start({ serverUrl, appId });
    // Let the app know it's started
    moralisStarted = true;
  }

  // Call configuration function when mounted
  onMount(() => {
    configureMoralis();
  });
</script>

<!-- If started, page content is loaded in this slot -->
{#if moralisStarted}
  <slot />
{/if}
Enter fullscreen mode Exit fullscreen mode

Using Svelte Stores

Svelte Stores are used to save information that you'll need to access from multiple areas of your application.

We'll create a currentUser store to track who's the current user logged in to our app.

Information on the User table in Moralis can only be accessed by the user themselves for privacy and security reasons. So we'll create a public profile table on our database to add public user details and use a Store to track that too.

// new file: /src/lib/stores.js

import { writable } from "svelte/store";

export const currentUser = writable('loading');
export const currentProfile = writable('loading');
Enter fullscreen mode Exit fullscreen mode

These don't look like much, but they're super powerful. We can import these stores and access their values in any part of our application with a $ before the store name (eg: $currentUser), that way they update the UI automatically when values change.

I'm initializing both stores with the value 'loading' so we know if no data was found or if we're still waiting for data to load.

Sign in with Wallet

Time to create our Sign in button in /src/lib/ButtonSignIn.svelte

<script>
  import { currentUser, currentProfile } from "$lib/stores";

  // Sign in function
  async function handleSignIn() {
    if ($currentUser) return;

    // Authenticate with MetaMask (Moralis API)
    const user = await Moralis.authenticate({
      signingMessage: "Sign in with Ethereum"
    });

    // Update value of current user store
    $currentUser = user;
  }

  // Sign out function
  async function handleSignOut() {
    await Moralis.User.logOut();
    $currentUser = null;
    $currentProfile = null;
  }
</script>

<!-- Still waiting for user data -->
{#if $currentUser == 'loading'}
  <p>Loading user…</p>

<!-- User is logged in, check for profile -->
{:else if $currentUser}

  <!-- profile still loading -->
  {#if $currentProfile == 'loading'}
    <p>Loading profile…</p>

  <!-- has a profile -->
  {:else if $currentProfile}
    <p>User is {$currentProfile.get("username")}</p>
    <button on:click={handleSignOut}>Sign out</button>

  <!-- no profile found -->
  {:else}
    <p>Create profile (To do…)</p>
  {/if}

<!-- User is logged out, show Sign in button-->
{:else}
  <button on:click={handleSignIn}>
    Sign in with MetaMask
  </button>
{/if}
Enter fullscreen mode Exit fullscreen mode

Here we're covering multiple states. We first look for the current user, then we look for that user's profile. If we have both, everything is fine and the user is fully logged in.

But we're stuck on loading. We still need to ask Moralis who's the current user (if there is one) when the page loads to know if we should show the log in button. This is something we need to happen on every page, so it should go on __layout.svelte.

<script>
  import "../app.css";
  import { onMount } from 'svelte';

  // Import stores
  import { currentUser, currentProfile } from "$lib/stores";

  // Import our sign in button
  import ButtonSignIn from "$lib/ButtonSignIn.svelte";

  let moralisStarted = false;

  // Get user when moralisStarted changes
  $: if (moralisStarted) getCurrentUser();

  // Get profile when value of $currentUser changes
  $: if ($currentUser) getCurrentProfile();

  async function configureMoralis() {...}

  // Check Moralis for current user
  async function getCurrentUser() {
    let user = Moralis.User.current();
    $currentUser = user;
    $currentProfile = 'loading';
  }

  // Check Moralis for current profile
  async function getCurrentProfile() {
    if ($currentUser == 'loading') return; // Ignore if still loading

    // Get Current Profile ID from Current User
    let id = $currentUser?.get("currentProfile")?.id;
    if (id) {

      // Get Profile from Moralis and update store
      const query = new Moralis.Query("Profile");
      $currentProfile = await query.get(id); // Profile or null

    } else {
      $currentProfile = null; // No profile created yet
    }
  }

  ...
</script>

{#if moralisStarted}

  <!-- Add sign in to top of the page -->
  <ButtonSignIn />
  <hr />

  <!-- Content for index.svelte and other pages will load here -->
  <slot />

{/if}
Enter fullscreen mode Exit fullscreen mode

The $: indicates a reactive statement in Svelte. It means the piece of code will run any time values change.

Creating a profile

Now we need to let the user create a profile after signing in. So let's create a form for that in a new file /src/lib/ProfileCreate.svelte.

<script>
  import { currentUser, currentProfile } from "$lib/stores";

  // Any data we want in our profile
  let profileData = {
    username: "",
    bio: ""
  };

  async function handleCreateProfile() {

    // Get reference to Profile object from Moralis
    const Profile = Moralis.Object.extend("Profile");

    // Create a new profile object
    let profile = new Profile();

    // Add data to profile object
    profile.set("username", profileData.username.toLowerCase());
    profile.set("bio", bio);
    profile.set("user", $currentUser);

    // Save profile in Moralis
    await profile.save();

    // Update profile store
    $currentProfile = profile;

    // Save current profile on currentUser in Moralis
    $currentUser.set("currentProfile", profile);
    await $currentUser.save();
  }
</script>

<p>
  Welcome, {$currentUser.get("ethAddress")}. <br />
  Please create a profile.
</p>
<p>
  <input type="text"
    bind:value={profileData.username}
    placeholder="Username"
  />
</p>
<p>
  <textarea
    bind:value={profileData.username}
    placeholder="Bio (optional)"
  />
</p>
<p>
  <button
    on:click={handleCreateProfile}
    disabled={!profileData.username}
  >
    Create profile
  </button>
</p>
Enter fullscreen mode Exit fullscreen mode

Svelte's data binding on the inputs mean the profileData variable is always in sync with what the user has typed in without any extra work. We're also disabling the button until the user adds some username.

Show profile creation

Now back in the ButtonSignIn component, we'll show the profile creation form when user is logged in but no profile has been found.

<script>
  import { currentUser, currentProfile } from "$lib/stores";

  // Import profile create form
  import ProfileCreate from "$lib/ProfileCreate.svelte";

  ...
</script>

{#if $currentUser == 'loading'}
  ...

{:else if $currentUser}

  {#if $currentProfile == 'loading'}
    ...

  {:else if $currentProfile}
    ...

  <!-- no profile found -->
  {:else}

    <!-- show create profile form -->
    <ProfileCreate />

  {/if}

{:else}
  ...
{/if}

Enter fullscreen mode Exit fullscreen mode

This already covers a lot of concepts you can use for adding more features. To add new tables and read data copy what we did for Profiles. To access data across your app create a new store.

Check out Svelte and Moralis docs for other details.

Leave a comment if something was unclear, you have questions, or with what you're trying to build and I can add a part 2 to this article. Follow me here or on Twitter to see when I post new articles. 👋

Top comments (0)