This is an example of how to use Netlify Identity with Netlify's PlanetScale integration and Next.js. This README.md will walk you through the most important steps to make it yourself! You can also just press the following button and follow steps 2 to 3 to deploy this example to your own Netlify account.
Let's create a basic Next.js site and deploy it to Netlify! In your terminal run:
npx create-next-app
Follow the default options, we went with using the src
directory. Then, let's deploy it to Netlify. Make sure to have the Netlify CLI installed and to have logged in with netlify login
. Then, run cd my-next-app
and connect your repository to a service like GitHub. After this run netlify init
and follow the prompts and then when you're done, run netlify open
to open your site's settings in your Netlify account.
In your Netlify Site Configuration, go to the Identity tab and enable Identity. We won't be using any external providers for this example.
Make sure you have a PlanetScale account and a database. The database should have the following table:
CREATE TABLE `issues` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`assignee_name` varchar(255) NOT NULL,
`assignee_email` varchar(255),
`status` enum('to-do', 'in progress', 'done') DEFAULT 'to-do',
PRIMARY KEY (`id`)
);
In your Netlify site view, go to Integrations and under Database enable PlanetScale. On the next page, click 'connect'. You'll be redirected to PlanetScale where you can authorize Netlify to access your database. Once you've done that, you'll be redirected back to Netlify and you can select your organization, database and configure which PlanetScale branch should be used in what environments for your Netlify site.
Now that we have Netlify Identity and PlanetScale enabled, let's configure our Next.js app to use them. First, let's install the dependencies we'll need:
npm install netlify-identity-widget @types/netlify-identity-widget @netlify/functions @netlify/planetscale
We'll be using React Context to make the Netlify Identity instance available to all components in our app. Create a new file src/context/authContext.tsx
and add the following contents:
// src/context/authContext.tsx
import netlifyIdentity, { type User } from 'netlify-identity-widget';
import { createContext, useEffect, useState } from 'react';
declare global {
interface Window {
netlifyIdentity: any;
}
}
interface NetlifyAuth {
isAuthenticated: boolean;
user: User | null;
initialize(callback: (user: User | null) => void): void;
authenticate(callback: (user: User) => void): void;
signout(callback: () => void): void;
}
const netlifyAuth: NetlifyAuth = {
isAuthenticated: false,
user: null,
initialize(callback) {
window.netlifyIdentity = netlifyIdentity;
netlifyIdentity.on('init', (user: User | null) => {
callback(user);
});
netlifyIdentity.init();
},
authenticate(callback) {
this.isAuthenticated = true;
netlifyIdentity.open();
netlifyIdentity.on('login', (user) => {
this.user = user;
callback(user);
netlifyIdentity.close();
});
},
signout(callback) {
this.isAuthenticated = false;
netlifyIdentity.logout();
netlifyIdentity.on('logout', () => {
this.user = null;
callback();
});
},
};
This will create a netlifyAuth
object that we can use to interact with Netlify Identity. Now let's create AuthContext
and use the netlifyAuth
object we just created.
// src/context/authContext.tsx
interface AuthContextType {
user: User | null;
login: () => void;
logout: () => void;
loading: boolean;
deleteAccount?: () => void;
}
export const AuthContext = createContext<AuthContextType>({
user: null,
login: () => {},
logout: () => {},
loading: false,
deleteAccount: () => {},
});
And then we'll create the provider:
// src/context/authContext.tsx
export const AuthContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [loggedIn, setLoggedIn] = useState<boolean>(
netlifyAuth.isAuthenticated
);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const login = () => {
netlifyAuth.authenticate((user) => {
setLoggedIn(!!user);
setUser(user);
});
};
const logout = () => {
netlifyAuth.signout(() => {
setLoggedIn(false);
setUser(null);
});
};
const deleteAccount = () => {
// TODO
};
useEffect(() => {
netlifyAuth.initialize((user: User | null) => {
setUser(user);
setLoggedIn(!!user);
});
setLoading(false);
}, [loggedIn]);
const contextValues = { user, login, logout, loading, deleteAccount };
return (
<AuthContext.Provider value={contextValues}>
{children}
</AuthContext.Provider>
);
};
Now inside of your app's layout file, in our case src/app/layout.tsx
, we'll wrap the app in the AuthContextProvider
:
// src/app/layout.tsx
'use client';
import { AuthContextProvider } from '@/context/authContext';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body>
<AuthContextProvider>{children}</AuthContextProvider>
</body>
</html>
);
}
This means that all children of the AuthContextProvider
will have access to the AuthContext
we created earlier. Now we'll be able to use the different functions from the AuthContext
to add login
and logout
buttons to our app.
// src/app/page.tsx
'use client';
import { AuthContext } from '@/context/authContext';
import { User } from 'netlify-identity-widget';
import { useContext, useEffect, useState } from 'react';
export default function Home() {
const { user, login, logout, loading } = useContext(AuthContext);
if (loading) return <div>Loading...</div>;
return user ? (
<>
<button onClick={logout}>Log out</button>
</>
) : (
<button onClick={login}>Log in / Register</button>
);
}
To try it out, commit your work and push it. The site will be redeployed. Now run netlify dev
and open your app in the browser. You should now be able to log in and log out! The first time you register it will send you through the production registration flow. This means that locally you'll have to log in with the credentials you set up on production to see your logged in state. You can also check the Netlify Identity tab in your Netlify Site Configuration view to see the users that have been created.
Now that we have Netlify Identity set up, let's add a form to create issues in our PlanetScale database. Add the following code to src/app/page.tsx
:
// src/app/page.tsx
'use client';
import { AuthContext } from '@/context/authContext';
import { User } from 'netlify-identity-widget';
import { useContext, useEffect, useState } from 'react';
export default function Home() {
const { user, login, logout, loading } = useContext(AuthContext);
const [issueTitle, setIssueTitle] = useState('');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIssueTitle(event.target.value);
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
try {
await fetch(`/.netlify/functions/create`, {
method: 'POST',
headers: {
// this will ensure that the Netlify function has access to the user object.
Authorization: `Bearer ${user?.token?.access_token}`,
},
body: JSON.stringify({
title: issueTitle,
}),
});
} catch (error) {
console.error('Error submitting issue:', error);
}
};
if (loading) return <div>Loading...</div>;
return user ? (
<>
{/* Our new form */}
<form onSubmit={handleSubmit}>
<label htmlFor='title'>Title:</label>
<input
onChange={handleInputChange}
type='text'
id='title'
name='title'
placeholder='Add an issue'
required
value={issueTitle}
/>
<button type='submit'>Submit</button>
</form>
<button onClick={logout}>Log out</button>
</>
) : (
<button onClick={login}>Log in / Register</button>
);
}
Now we'll need to create a serverless function to handle the form submission. Create a new file netlify/functions/create.ts
and add the following code:
// netlify/functions/create.ts
import connection from '@netlify/planetscale';
import type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions';
const handler: Handler = async function (
event: HandlerEvent,
context: HandlerContext
) {
// We get the user from the context's clientContext
const { user } = context.clientContext as {
identity: { url: string; token: string };
user: { sub: string; email: string; user_metadata: { full_name: string } };
};
if (!event.body) {
return {
statusCode: 400,
body: 'Please provide a title for the issue',
};
}
// This checks wether the user is logged in or not
if (!user?.sub || !user?.email) {
return {
statusCode: 401,
body: 'Unauthorized',
};
}
const body = JSON.parse(event.body);
// We insert the issue into the PlanetScale database using the user's name and email from the user object
return connection
.execute(
`
INSERT INTO issues (title, assignee_name, assignee_email)
VALUES (?, ?, ?)
`,
[body.title, user.user_metadata.full_name, user.email]
)
.then(() => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Issue created' }),
};
})
.catch((error) => {
return {
statusCode: 500,
body: `Internal Server Error: ${error}`,
};
});
};
export { handler };
It should now be possible to create issues in your PlanetScale database! Try adding one and then checking your PlanetScale database to see if it worked.
Now that we can create issues, let's add a list of issues to our app. We'll create a new serverless function to get all issues from our PlanetScale database. Create a new file netlify/functions/get.ts
and add the following code:
// netlify/functions/get.ts
import type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions';
import connection from '@netlify/planetscale';
const handler: Handler = async function (
_event: HandlerEvent,
context: HandlerContext
) {
// We get the user from the context's clientContext
const { user } = context.clientContext as {
identity: { url: string; token: string };
user: { sub: string; email: string };
};
// This checks wether the user is logged in or not
if (!user?.sub || !user?.email) {
return {
statusCode: 401,
body: 'Unauthorized',
};
}
// We get all issues from the PlanetScale database
return await connection
.execute(`SELECT * FROM issues`)
.then((issues) => {
const { rows } = issues;
return {
statusCode: 200,
body: JSON.stringify(rows),
};
})
.catch((error) => {
return {
statusCode: 500,
body: `Internal Server Error: ${error}`,
};
});
};
export { handler };
Now inside of src/app/page.tsx
we'll add a new useEffect
hook to fetch the issues from our PlanetScale database. We'll also add a new state variable to store the issues in. After a new issue is submitted we'll also fetch the issues again to update the page. The code will look like this:
// src/app/page.tsx
'use client';
import { AuthContext } from '@/context/authContext';
import { User } from 'netlify-identity-widget';
import { useContext, useEffect, useState } from 'react';
interface Issue {
id: string;
title: string;
}
const fetchIssues = async (user: User) => {
const res = await fetch('/.netlify/functions/get', {
headers: {
Authorization: `Bearer ${user?.token?.access_token}`,
},
});
const data = await res.json();
return data;
};
export default function Home() {
const { user, login, logout, loading } = useContext(AuthContext);
const [issues, setIssues] = useState<Issue[]>([]);
const [loadingIssues, setLoadingIssues] = useState<boolean>(true);
const [issueTitle, setIssueTitle] = useState('');
useEffect(() => {
if (user) {
fetchIssues(user).then((issues) => {
setIssues(issues);
setLoadingIssues(false);
});
}
}, [user]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIssueTitle(event.target.value);
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
try {
await fetch(`/.netlify/functions/create`, {
method: 'POST',
headers: {
// this will ensure that the Netlify function has access to the user object.
Authorization: `Bearer ${user?.token?.access_token}`,
},
body: JSON.stringify({
title: issueTitle,
}),
});
if (user) {
fetchIssues(user).then((issues) => {
setIssues(issues);
});
}
} catch (error) {
console.error('Error submitting issue:', error);
}
};
if (loading || loadingIssues) return <div>Loading...</div>;
return user ? (
<>
{/* Our new form */}
<form onSubmit={handleSubmit}>
<label htmlFor='title'>Title:</label>
<input
onChange={handleInputChange}
type='text'
id='title'
name='title'
placeholder='Add an issue'
required
value={issueTitle}
/>
<button type='submit'>Submit</button>
</form>
{/* List our issues */}
<ul>
{issues.map((issue) => (
<li key={issue.id}>{issue.title}</li>
))}
</ul>
<button onClick={logout}>Log out</button>
</>
) : (
<button onClick={login}>Log in / Register</button>
);
}
We want to make sure that users can also delete their account and issues. Let's make a new serverless function in netlify/functions/delete.ts
. In that file we'll check if the user is currently logged in and authenticated, and then we delete that user Netlify Identity, and delete all of their entries from our PlanetScale database. The code will look like this:
// netlify/functions/delete.ts
import type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions';
import connection from '@netlify/planetscale';
const handler: Handler = async function (
_event: HandlerEvent,
context: HandlerContext
) {
const { identity, user } = context.clientContext as {
identity: { url: string; token: string };
user: { sub: string; email: string };
};
if (!user?.sub || !user?.email) {
return {
statusCode: 401,
body: 'Unauthorized',
};
}
const userUrl = `${identity.url}/admin/users/{${user.sub}}`;
const adminAuthHeader = `Bearer ${identity.token}`;
return fetch(userUrl, {
method: 'DELETE',
headers: { Authorization: adminAuthHeader },
})
.then(async () => {
console.log('Deleted a user!');
await connection.execute(
`
DELETE FROM issues
WHERE assignee_email = ?
`,
[user.email]
);
})
.then(() => {
console.log('Deleted all issues assigned to the user!');
return { statusCode: 204 };
})
.catch((error) => ({
statusCode: 500,
body: `Internal Server Error: ${error}`,
}));
};
export { handler };
Inside the deleteAccount
function in the authContext
file, we'll do a call to the function we just created. We'll first ask the user to confirm they want to delete their account, and then we'll call the serverless function. The code will look like this:
// src/context/authContext.tsx
const deleteAccount = () => {
if (
window.confirm(
'Are you sure? This will delete the issues you created and your account. '
)
) {
fetch('/.netlify/functions/delete', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${user?.token?.access_token}`,
},
})
.then(async () => logout())
.catch((err) => console.error(err));
}
};
This function is exported in the AuthContext
provider, so you can use it in any children of the AuthContextProvider
. For example by providing a button that calls the function.
// src/app/page.tsx
'use client';
import { AuthContext } from '@/context/authContext';
import { User } from 'netlify-identity-widget';
import { useContext, useEffect, useState } from 'react';
interface Issue {
id: string;
title: string;
}
const fetchIssues = async (user: User) => {
const res = await fetch('/.netlify/functions/get', {
headers: {
Authorization: `Bearer ${user?.token?.access_token}`,
},
});
const data = await res.json();
return data;
};
export default function Home() {
const { user, login, logout, loading, deleteAccount } =
useContext(AuthContext);
const [issues, setIssues] = useState<Issue[]>([]); // Provide type for issues state variable
const [loadingIssues, setLoadingIssues] = useState<boolean>(true);
const [issueTitle, setIssueTitle] = useState('');
useEffect(() => {
if (user) {
fetchIssues(user).then((issues) => {
setIssues(issues);
setLoadingIssues(false);
});
}
}, [user]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIssueTitle(event.target.value);
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
try {
await fetch(`/.netlify/functions/create`, {
method: 'POST',
headers: {
// this will ensure that the Netlify function has access to the user object.
Authorization: `Bearer ${user?.token?.access_token}`,
},
body: JSON.stringify({
title: issueTitle,
}),
});
} catch (error) {
console.error('Error submitting issue:', error);
}
};
if (loading || loadingIssues) return <div>Loading...</div>;
return user ? (
<>
{/* Our new form */}
<form onSubmit={handleSubmit}>
<label htmlFor='title'>Title:</label>
<input
onChange={handleInputChange}
type='text'
id='title'
name='title'
placeholder='Add an issue'
required
value={issueTitle}
/>
<button type='submit'>Submit</button>
</form>
{/* List our issues */}
<ul>
{issues.map((issue) => (
<li key={issue.id}>{issue.title}</li>
))}
</ul>
<button onClick={logout}>Log out</button>
<button onClick={deleteAccount}>Delete account</button>
</>
) : (
<button onClick={login}>Log in / Register</button>
);
}
Congrats! You now have a working example! You can start building your own app! Here are some ideas:
- Add a form to update issues
- Add a button to delete issues
- Make it possible to change the status of an issue
Are you curious about building your own integration with the Netlify platform? Then check out our Netlify SDK