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
- If you don't have NPM installed read this.
- 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"
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>
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}
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');
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}
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}
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>
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}
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)