In an ideal scenario, A Dapp should not have any backend server, everything should be on-chain. centralized Then why Backend? With a fully on-chain approach, we need to (at least right now) compromise on user experience Not everything needs to be on a chain, it just doesn’t make sense. Wouldn’t will it be great if we can make our apps sufficiently decentralized? There is a great By on Sufficient Decentralization which everyone should give read Article Varun Srinivasan Better scalability: A backend can help a Dapp scale more easily by handling tasks that might otherwise slow down the blockchain or consume a large amount of gas. Off-chain computation: Some tasks, such as image or video processing, can be resource-intensive and may not be practical to perform on the blockchain. A backend can handle these tasks and communicate with the Dapp as needed. For this or any reason you might need to have the backend connected to your Dapp, but how? let’s get started with it So, what are we building Deep? 👀 A Dapp which connects to your backend maintains a session and connects with the Database. Watch a demonstration . here What is SIWE (Sign-In with Ethereum)? Sign-In with Ethereum describes how Ethereum accounts to authenticate with off-chain services by signing a standard message format parameterized by scope, session details, and security mechanisms (e.g., a nonce). This specification aims to provide a self-custodied alternative to centralized identity providers, improve interoperability across off-chain services for Ethereum-based authentication, and provide wallet vendors with a consistent machine-readable message format to achieve enhanced user experiences and consent management. More details can be found . here Here are some steps we will be following Connect wallet Sign SIWE message with the nonce generated by the backend Verify the submitted SIWE message and signature via the POST request Add validated SIWE fields to the session (via JWT, cookie, etc.) Store user details(eg: name) in MongoDB Update user information Maintaining sessions b/w page refreshes Tools We are going to use: — Component library, of course, I hate CSS and I’m not going to write any of it NextUI — React Hooks for Ethereum Wagmi Iron Session Clone the code from the , to follow through GitHub repo Step1: Let’s create some API routes first under create a file nonce.ts with the following content pages/api pages/api/nonce.ts import { withIronSessionApiRoute } from 'iron-session/next' import { sessionOptions } from 'lib/session' import { NextApiRequest, NextApiResponse } from 'next' import { generateNonce } from 'siwe' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { method } = req switch (method) { case 'GET': req.session.nonce = generateNonce() await req.session.save() res.setHeader('Content-Type', 'text/plain') res.send(req.session.nonce) break default: res.setHeader('Allow', ['GET']) res.status(405).end(`Method ${method} Not Allowed`) } } export default withIronSessionApiRoute(handler, sessionOptions) Next, add an API route to verify a SIWE message and make the user session. pages/api/verify.ts import { handleLoginOrSignup } from 'core/services/user.service' import { withIronSessionApiRoute } from 'iron-session/next' import dbConnect from 'lib/dbConnect' import { sessionOptions } from 'lib/session' import { NextApiRequest, NextApiResponse } from 'next' import { SiweMessage } from 'siwe' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { method } = req switch (method) { case 'POST': try { await dbConnect(); const { message, signature } = req.body const siweMessage = new SiweMessage(message) const fields = await siweMessage.validate(signature) if (fields.nonce !== req.session.nonce) { return res.status(422).json({ message: 'Invalid nonce.' }) } req.session.siwe = fields; // maintaining users details in MongoDB from below line // we will get backto this later ignore for now const user = await handleLoginOrSignup(fields.address); req.session.user = user; await req.session.save() res.json({ ok: true }) } catch (_error) { console.log('error -> verify', _error) res.json({ ok: false, error: _error }) } break default: res.setHeader('Allow', ['POST']) res.status(405).end(`Method ${method} Not Allowed`) } } export default withIronSessionApiRoute(handler, sessionOptions) const { message, signature } = req.body const siweMessage = new SiweMessage(message) const fields = await siweMessage.validate(signature) We will be passing a message and signature done by the user's wallet from the Frontend, on the API side we are verifying it and the nonce, nonce will prevent the replay attacks. our iron session config file is going to look something like // this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions import type { IronSessionOptions } from "iron-session"; import { IUser } from "models/User"; export const sessionOptions: IronSessionOptions = { password: 'passowrd_cookie', cookieName: "iron-session/examples/next.js", cookieOptions: { secure: process.env.NODE_ENV === "production", }, }; // This is where we specify the typings of req.session.* declare module "iron-session" { interface IronSessionData { siwe: any; nonce: any; user: IUser | undefined; // users details from DB } } Now, let’s create an API for checking user’s sessions and returning users details if a user is already logged in Note: is a helper method that is getting user's details from MongoDB, you can check out the code from GitHub, to explore this helper method pages/api/me.ts findUserByAddress import { findUserByAddress } from 'core/services/user.service' import { ReasonPhrases, StatusCodes } from 'http-status-codes' import { withIronSessionApiRoute } from 'iron-session/next' import { sessionOptions } from 'lib/session' import { NextApiRequest, NextApiResponse } from 'next' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { method } = req switch (method) { case 'GET': const address = req.session.siwe?.address if (address) { const user = await findUserByAddress(address) res.json({ address: req.session.siwe?.address, user }) return } res.status(StatusCodes.UNAUTHORIZED).json({ message: ReasonPhrases.UNAUTHORIZED, }) break default: res.setHeader('Allow', ['GET']) res.status(405).end(`Method ${method} Not Allowed`) } } export default withIronSessionApiRoute(handler, sessionOptions) And lastly route, where we are destroying the session logout import { withIronSessionApiRoute } from 'iron-session/next' import { sessionOptions } from 'lib/session' import { NextApiRequest, NextApiResponse } from 'next' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { method } = req switch (method) { case 'POST': await req.session.destroy() res.send({ ok: true }) break default: res.setHeader('Allow', ['POST']) res.status(405).end(`Method ${method} Not Allowed`) } } export default withIronSessionApiRoute(handler, sessionOptions) Now, let’s create a state file for maintaining all user data and related methods we are going to use context API for this import { IUser } from 'models/User' import React, { createContext, useContext, useState } from 'react' import { SiweMessage } from 'siwe' import { Connector, useAccount, useConnect } from 'wagmi' import axios from 'axios' import { useRouter } from 'next/router' import { toast } from 'react-toastify' export interface IUserState { user: IUser | undefined loadingUser: boolean setUser: React.Dispatch<React.SetStateAction<undefined>> handleSignOut: () => void handleSignIn: (connector: Connector) => Promise<void> } const UserContext = createContext<IUserState>({ user: undefined, setUser: () => {}, loadingUser: false, handleSignOut: () => {}, handleSignIn: async () => {}, }) export function UserState({ children }: { children: JSX.Element }) { const router = useRouter() const [user, setUser] = useState(undefined) const [, connect] = useConnect() const [loadingUser, setLoadingUser] = useState(false) const [, disconnect] = useAccount({ fetchEns: true, }) const handleSignOut = async () => { disconnect() await axios.post('/api/logout') setUser(undefined) router.replace('/') } const handleSignIn = async (connector: Connector) => { try { const res = await connect(connector) // connect from useConnect if (!res.data) throw res.error ?? new Error('Something went wrong') setLoadingUser(true) const nonceRes = await axios('/api/nonce') const message = new SiweMessage({ domain: window.location.host, address: res.data.account, statement: 'Sign in with Ethereum to the app.', uri: window.location.origin, version: '1', chainId: res.data.chain?.id, nonce: nonceRes.data, }) const signer = await connector.getSigner() const signature = await signer.signMessage(message.prepareMessage()) // console.log('message', message, { signature }) await axios.post('/api/verify', { message, signature, }) const me = await axios('/api/me') setUser(me.data.user) // It worked! User is signed in with Ethereum } catch (error) { // Do something with the error toast.error('Something went wrong!') handleSignOut() console.log('error', error) } finally { setLoadingUser(false) } } return ( <UserContext.Provider value={{ user, setUser, handleSignOut, handleSignIn, loadingUser }} > {children} </UserContext.Provider> ) } export function useUserContext() { return useContext(UserContext) } Breaking down method handleSignIn const res = await connect(connector) // connect from useConnect if (!res.data) throw res.error ?? new Error('Something went wrong') we are connected to the wallet here Once the user is connected we generate a random nonce using the API we created earlier const nonceRes = await axios('/api/nonce') const message = new SiweMessage({ domain: window.location.host, address: res.data.account, // users waller address statement: 'Sign in with Ethereum to the app.', uri: window.location.origin, version: '1', chainId: res.data.chain?.id, nonce: nonceRes.data, }) const signer = await connector.getSigner() const signature = await signer.signMessage(message.prepareMessage()) then we ask the user to sign a message, using constructor exposed by SIWE and creates a signature SiweMessage await axios.post('/api/verify', { message, signature, }) const me = await axios('/api/me') setUser(me.data.user) Verifying users, and getting logged in user’s data using API we created earlier, then set the user's data to state me Now let’s make the API for updating user details in the Database, import withAuth from 'core/middleware/withAuth' import { findUserById, updateUser } from 'core/services/user.service' import { ReasonPhrases, StatusCodes } from 'http-status-codes' import { withIronSessionApiRoute } from 'iron-session/next' import { sessionOptions } from 'lib/session' import { isValidObjectId } from 'mongoose' import { NextApiRequest, NextApiResponse } from 'next' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { method, query } = req const queryId = query.id; if (!queryId || !isValidObjectId(queryId)) { res.status(StatusCodes.BAD_REQUEST).json({ message: "Valid id is required" }) return; } switch (method) { case 'PUT': return await handlePatchUser(); default: res.setHeader('Allow', ['PUT']) res.status(405).end(`Method ${method} Not Allowed`) }; async function handlePatchUser() { if (req.session.user?._id !== queryId) { res.status(StatusCodes.FORBIDDEN).json({ message: ReasonPhrases.FORBIDDEN }); return; } const user = await updateUser(queryId, req.body); if (!user) { res.status(StatusCodes.BAD_REQUEST).json({ message: "User not found with requested id" }) } res.json({ user }) } } export default withIronSessionApiRoute(withAuth(handler), sessionOptions) Notice we are using here it is a we have created, so only authorized users can access our API routes withAuth middleware Link to code Now let’s create a profile in pages/profile.ts import type { NextPage } from 'next' import Head from 'next/head' import { BaseLayout } from 'components/ui/Layout/BaseLayout' import { ComponentWithLayout } from '../_app' import { useFormik } from 'formik' import axios from 'axios' import { useUserContext } from 'core/state/user.state' import { useState } from 'react' import { Button, Loading } from '@nextui-org/react' import { toast } from 'react-toastify' const Profile: NextPage = () => { const { user } = useUserContext() const [loading, setLoading] = useState(false) const formik = useFormik({ enableReinitialize: true, initialValues: { ...user, }, onSubmit: async (values) => { try { setLoading(true) await axios.put(`/api/users/${user?._id}`, values) toast.success('Data saved successfully') } catch (error: any) { toast.error(error.message) } finally { setLoading(false) } }, }) return ( <div className="flex flex-col items-center justify-center py-2"> <Head> <title>Profile</title> <link rel="icon" href="/favicon.ico" /> </Head> <section className="bg-blueGray-100 rounded-b-10xl"> <div className="container mx-auto px-4"> <div className="-mx-4 flex flex-wrap"> <div className="w-full px-4"> <div className="mx-auto max-w-xl rounded-xl bg-white py-14 px-8 md:px-20 md:pt-16 md:pb-20"> <h3 className="font-heading mb-12 text-4xl font-medium"> Profile Details </h3> <input className="placeholder-darkBlueGray-400 mb-5 w-full rounded-xl border px-12 py-5 text-xl focus:bottom-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" type="text" placeholder="Your Name" name="name" onChange={formik.handleChange} value={formik.values.name} /> <div className="text-right"> <Button clickable={!loading} color="primary" className="inline-block w-full text-center text-xl font-medium tracking-tighter md:w-auto" onClick={formik.submitForm} size="lg" icon={ loading && ( <Loading type="spinner" color="white" size="md" /> ) } > Save </Button> </div> </div> </div> </div> </div> </section> </div> ) } export default Profile ;(Profile as ComponentWithLayout).Layout = BaseLayout And that’s a wrap! Also published . here