Reference for this experiment: https://www.youtube.com/watch?v=rediFff54JA&t=75s
This experiment was initiated on April 9, 2023.
- Introduction to PocketBase
- How to Run PocketBase?
- Explaining the Links
- Folders in a Pre-Built Pocket Base
- User-Defined Folder
- Adding a React Application to a PocketBase App
- Regaining Access to the PocketBase Admin UI
- Dangers of Deleting pb_data
- Creating a New Collection
- React and PocketBase SDK Setup
- Authentication (with react-hook-form)
• Example #1
• Example #2
• Example #3 - Auth Hooks (with react-query)
• Destructuring Logout Function
• Destructuring Login Function
• Destructuring API call to PocketBase - Sending Verification Emails with Brevo (formerly Sendinblue) SMTP
• Setting up an SMTP Server in PocketBase
• Setting up an SMTP Server in PocketBase
• Error in Setting up PocketBase Mail Settings
• PocketBase Collection Types
• Sending the Email Verification - Automatic Re-fetching with the useQuery Hook
• Explaining the Attributes of the useQuery Option
PocketBase
is similar to Firebase
(a Google service) and Supabase
(an open-source Firebase alternative), which handle backend functions like file upload, saving data to a database, etc.
I know that my introduction to PocketBase was too short to describe it, so here is the link to a video of Fireship where he fully introduces PocketBase.
You can download PocketBase
using this link.
Once you've downloaded PocketBase
and it was now in Downloads
, do the following:
1. Extract the ZIP file that you downloaded.
2. Rename it PocketBase
or however you like, but keep it short.
3. Go to that directory, e.g., Downloads/PocketBase
.
4. Now enter this command:
./pocketbase serve
If the command above didn't work for you, try this:
pocketbase serve
Note: The pocketbase
you see in the command is not the folder name, it was the keyword for the command.
5. After running the previous command you will see something like this:
2023/04/09 22:11:46 Server started at http://127.0.0.1:8090
➜ REST API: http://127.0.0.1:8090/api/
➜ Admin UI: http://127.0.0.1:8090/_/
Based on the official documentation, the meaning of the links are the following:
• http://127.0.0.1:8090
- if pb_public directory exists, serves the static content from it (html, css, images, etc.)
• http://127.0.0.1:8090/_/
- Admin dashboard UI
• http://127.0.0.1:8090/api/
- REST API
The prebuilt PocketBase executable will automatically create and manage 2 new directories alongside the executable:
• pb_data
- stores your application data, uploaded files, etc. (usually should be added in .gitignore
).
• pb_migrations
- contains JS migration files with your collection changes (can be safely committed in your repository).
You have to add the following folder:
pb_public
- This is the folder that will be hosted by PocketBase. Here is an example:
1. After creating the pb_public
folder, create a file inside this folder named index.html
and copy and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Testing123</title>
</head>
<body>
<h1>Welcome to PocketBase Experiment by MadriñanComLab!</h1>
</body>
</html>
2. Then go to this URL:
http://127.0.0.1:8090/
To add the React app, just do the following command:
npm create react-app <app_name>
Example:
npm create react-app pb_app
Just in case you have forgotten your password. What you can do is:
1. Stop the server of PocketBase on your computer (if it is running).
2. Delete the pb_data
3. Run your PocketBase
4. Go to http://127.0.0.1:8090/_/
5. Then you can now set your new email and password.
Deleting this folder was helpful when you forgot your password for the admin UI, but deleting it will also affect the configuration you set in your PocketBase application.
Later on, configuring the mail settings of PocketBase will be discussed in this documentation.
Additional Note: By deleting the pb_data
, you are deleting the data in your database. DO NOT delete pb_migration
because this contains the structure of your database. You may delete it unless you intend to delete the structure of the database. But to just regain access to the admin side of your database, just delete the pb_data
.
The admin UI was straightforward, but in case you are hesitant, here is a brief introduction:
1. Start with creating your collection. After you click that, a modal will appear on the right.
2. Name your collection however you want. Think of a collection as a table, and you can see in the suggestion that it follows the naming convention of SQL (where tables are named plural).
3. After naming your first collection, you can then create the first field in your collection.
4. After clicking the New field
button, options for data type would appear.
5. Name your first field here. Think of a field as a column in your database.
6. If you click the cog icon inline with your field, an option for min and max length would appear as well as the regex.
7. If you click "New index," a modal will appear for you to setup your indexes.
Unfamiliar with indexes? You can read about it here
8. You can now save your first collection. Ow, wait, you aren't able to save it? Well, that was because there is already a "users" collection that was initially included in your first PocketBase application.
I apologize about it; this README was created while first exploring PocketBase.
In the tutorial, the following files were added to the root folder of pb_app
:
• .env.local
• jsconfig.json - In the code here, what it does is make the src
folder the root directory for importing and avoid the example imports below.:
import x from "../../../folder/file";
• Delete the following in pb_app/src
:
• App.css
• App.test.js
• index.css
• logo.svg
• reportWebVitals.js
• setupTest.js
• Remove the following in index.js
:
import reportWebVitals from './reportWebVitals';
import './index.css';
...
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
• Remove the following in App.js
:
import logo from './logo.svg';
import './App.css';
...
// This is the chuck of code in return statement
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
• You can now add the JavaScript PocketBase SDK by running the following command in your terminal:
npm install pocketbase --save
Note: Make sure you are in the pb_app
directory, where the React app is.
• Then create the /lib/pocketbase.js
file inside the src
folder and add the following code snippet:
import PocketBase from "pocketbase";
const pb = new PocketBase(process.env.REACT_APP_PB_URL);
export default pb;
Note: The value of REACT_APP_PB_URL
was http://127.0.0.1:8090
, and it was in the .env.local
• Now, to test if everything is working, run the following command:
npm start
PocketBase
provides documentation about their authentication; you can read it here if you want to read more about it.
The following will be a set of different examples of PocketBase
authentication.
First, create Auth.js
in src
. For now, the Auth component will contain the following:
import PB from "lib/pocketbase";
function Auth(){
return (
<>
{/* For a simple example of pocketbase authentication */}
<h1>Logged In: { PB.authStore.isValid.toString() }</h1>
</>
);
}
export default Auth;
Import the Auth component to App.js:
import Auth from "Auth";
function App() {
return (
<>
<Auth/>
</>
);
}
export default App;
Your browser should display something like this:
The first example makes sense that it displays false
because a user hasn't logged in yet, right?
Now, let's level up the previous example. Still in Auth.js
, do the following:
import PB from "lib/pocketbase";
import { useState } from "react";
import { useForm } from "react-hook-form";
function Auth(){
const { register, handleSubmit } = useForm();
const [ is_loading, setLoading ] = useState(false);
async function login(data){
/* This function was a custom function of handleSubmit() */
setLoading(true);
try{
const auth_data = await PB
.collection("users")
.authWithPassword(data.email, data.password);
}
catch(error){
console.log(error);
}
setLoading(false);
}
return (
<>
<h1>Logged In: { PB.authStore.isValid.toString() }</h1>
{ is_loading && <p>Loading...</p> }
<form onSubmit={ handleSubmit(login) }>
<input type="text" placeholder="email" {...register("email")}/>
<input type="password" placeholder="password" {...register("password")}/>
<button type="submit" disabled={ is_loading }>Login</button>
</form>
</>
);
}
export default Auth;
Before we proceed with creating the first user, let's breakdown the example code above because there was a huge change between examples #1
and #2
.
- First is about the
React-hook-form
, which will help us handle the sample login:
const { register, handleSubmit } = useForm();
To install react-hook-form
in your project, just run the following command:
npm install react-hook-form
Note: Make sure you are in pb_app
folder before you install this npm library.
- State object that will be used for a simple loading indicator.
const [ is_loading, setLoading ] = useState(false);
- Then, on the actual code for validation, there was a comment on the code to help break it down:
async function login(data){
/* This function was a custom function of handleSubmit() */
/* Set is_loading to true to indicated the request in on process */
setLoading(true);
/* Try catch statement was used to handle the possible error that may occur. */
try{
const auth_data = await PB
/* The 'collection' is the collection of pocketbase */
.collection("users")
/* 'authWithPassword' will handle the authentication */
.authWithPassword(data.email, data.password);
}
catch(error){
console.log(error);
}
setLoading(false);
}
Now, let's create our first user. You can use whatever email and password you want:
After this, you can try it to see if it works.
Users can now log in, and for the last example, we will enable users to log out. Do this in Auth.js
.:
import PB from "lib/pocketbase";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
function Auth(){
const { register, handleSubmit } = useForm();
const [ is_loading, setLoading ] = useState(false);
const is_logged_in = PB.authStore.isValid;
/* Later, this will be use to re-render the Auth component */
const [ dummy, setDummy] = useState(0);
async function login(data){
/* This function was a custom function of handleSubmit() */
setLoading(true);
try{
const auth_data = await PB
.collection("users")
.authWithPassword(data.email, data.password);
}
catch(error){
console.log(error);
}
setLoading(false);
}
function logout(){
/* logout() function was not an asynchronous function because it doesn't do an API call. Instead, it simply clear the cookie */
PB.authStore.clear();
setDummy(Math.random());
}
/* Just another way of conditional rendering in React */
if(is_logged_in){
return (
<>
{/* If a user has logged in, display the email address */}
<h1>Logged In: { is_logged_in && PB.authStore.model.email }</h1>
<button onClick={ logout }>Log Out</button>
</>
);
}
return (
<>
<h1>Welcome to Login Page!</h1>
{ is_loading && <p>Loading...</p> }
<form onSubmit={ handleSubmit(login) }>
<input type="text" placeholder="email" {...register("email")}/>
<input type="password" placeholder="password" {...register("password")}/>
<button type="submit" disabled={ is_loading }>Login</button>
</form>
</>
);
}
export default Auth;
In this part of the documentation, we will be destructuring the example code from previous chapter.
First, we'll destructure the function for logging out. Create a hooks
folder in src
and create UseLogout.js
, which will contain the following code:
import PB from "lib/pocketbase";
import { useState } from "react";
function UseLogout(){
/* Later, this will be use to re-render the Auth component */
const [ dummy, setDummy] = useState(0);
function logout(){
/* logout() function was not an asynchronous function because it doesn't do an API call. Instead, it simply clear the cookie */
PB.authStore.clear();
setDummy(Math.random());
}
return logout;
}
export default UseLogout;
Note: The code above was an example of React custom hooks.
The Auth.js
will look like this:
/* The custom hook was imported to Auth.js */
import UseLogout from "hooks/UseLogout";
import PB from "lib/pocketbase";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
function Auth(){
/* logout() function was declared like this */
const logout = UseLogout();
const { register, handleSubmit } = useForm();
const [ is_loading, setLoading ] = useState(false);
const is_logged_in = PB.authStore.isValid;
async function login(data){
/* This function was a custom function of handleSubmit() */
setLoading(true);
try{
const auth_data = await PB
.collection("users")
.authWithPassword(data.email, data.password);
}
catch(error){
console.log(error);
}
setLoading(false);
}
if(is_logged_in){
return (
<>
{/* If a user has logged in, display the email address */}
<h1>Logged In: { is_logged_in && PB.authStore.model.email }</h1>
<button onClick={ logout }>Log Out</button>
</>
);
}
return (
<>
<h1>Welcome to Login Page!</h1>
{ is_loading && <p>Loading...</p> }
<form onSubmit={ handleSubmit(login) }>
<input type="text" placeholder="email" {...register("email")}/>
<input type="password" placeholder="password" {...register("password")}/>
<button type="submit" disabled={ is_loading }>Login</button>
</form>
</>
);
}
export default Auth;
If you try this, it will work well, but the same problem as in the previous chapter will remain: the credentials you've inputted will remain.
To resolve this, add reset
in the destructuring of useForm()
:
const { register, handleSubmit, reset } = useForm();
Then, add this reset()
function to the login function:
async function login(data){
/* This function was a custom function of handleSubmit() */
setLoading(true);
try{
const auth_data = await PB
.collection("users")
.authWithPassword(data.email, data.password);
}
catch(error){
console.log(error);
}
setLoading(false);
/* reset() was added here */
reset();
}
Now that we've destructured the logout function using a custom hook, we'll take a few steps to destructure the login as well.
Create UseLogin.js
and it will contain the following:
import PB from "lib/pocketbase";
import { useState } from "react";
function UseLogin(){
const [ is_loading, setLoading ] = useState(false);
async function login({ email, password }){
/* This function was a custom function of handleSubmit() */
setLoading(true);
try{
const auth_data = await PB
.collection("users")
.authWithPassword(email, password);
}
catch(error){
console.log(error);
}
setLoading(false);
}
return { login, is_loading };
}
export default UseLogin;
Since we removed the login function, Auth.js
will look like this:
import UseLogin from "hooks/UseLogin";
import UseLogout from "hooks/UseLogout";
import PB from "lib/pocketbase";
import { useForm } from "react-hook-form";
function Auth(){
const logout = UseLogout();
const { login, is_loading } = UseLogin();
const { register, handleSubmit, reset } = useForm();
const is_logged_in = PB.authStore.isValid;
async function onSubmit(data){
login(data);
reset();
}
if(is_logged_in){
return (
<>
{/* If a user has logged in, display the email address */}
<h1>Logged In: { is_logged_in && PB.authStore.model.email }</h1>
<button onClick={ logout }>Log Out</button>
</>
);
}
return (
<>
<h1>Welcome to Login Page!</h1>
{ is_loading && <p>Loading...</p> }
<form onSubmit={ handleSubmit(onSubmit) }>
<input type="text" placeholder="email" {...register("email")}/>
<input type="password" placeholder="password" {...register("password")}/>
<button type="submit" disabled={ is_loading }>Login</button>
</form>
</>
);
}
export default Auth;
In this part of the documentation, we will be using the react-query
, and to install this library, run this command:
npm install react-query
Before we proceed to destructuring, we have to do the following on App.js
to be able to implement the react-query
and do the destructuring:
import Auth from "Auth";
/* Import the necessary functions */
import { QueryClientProvider, QueryClient } from "react-query";
/* Initialize the QueryClient */
const queryClient = new QueryClient();
function App() {
return (
{/* Wrap the Auth component with QueryClientProvider and use queryClient as client */}
<QueryClientProvider client={ queryClient }>
<Auth/>
</QueryClientProvider>
);
}
export default App;
UseLogin.js
will look like this after the destructuring:
import PB from "lib/pocketbase";
/* useMutation was imported */
import { useMutation } from "react-query"
/* useState for is_loading was removed because react-query has built-in and it was isLoading*/
function UseLogin(){
/* Because login() function was used as argument to useMutation, we no longer need to wrap it with try catch */
async function login({ email, password }){
/* This function was a custom function of handleSubmit() */
const auth_data = await PB
.collection("users")
.authWithPassword(email, password);
}
/* login() was passed in useMutation as argument */
return useMutation(login)
}
export default UseLogin;
And because of these changes, Auth.js
will look like this:
import UseLogin from "hooks/UseLogin";
import UseLogout from "hooks/UseLogout";
import PB from "lib/pocketbase";
import { useForm } from "react-hook-form";
function Auth(){
const logout = UseLogout();
const {
/* "mutate" will be used to invoke the login() function */
mutate,
/* "is_loading" is now isLoading() which is available to useMutation() by default */
isLoading,
/* isError was also available to useMutation() by default */
isError
} = UseLogin();
const { register, handleSubmit, reset } = useForm();
const is_logged_in = PB.authStore.isValid;
async function onSubmit(data){
mutate(data);
reset();
}
if(is_logged_in){
return (
<>
{/* If a user has logged in, display the email address */}
<h1>Logged In: { is_logged_in && PB.authStore.model.email }</h1>
<button onClick={ logout }>Log Out</button>
</>
);
}
return (
<>
<h1>Welcome to Login Page!</h1>
{ isLoading && <p>Loading...</p> }
{ isError && <p>Invalid email or password</p> }
<form onSubmit={ handleSubmit(onSubmit) }>
<input type="text" placeholder="email" {...register("email")}/>
<input type="password" placeholder="password" {...register("password")}/>
<button type="submit" disabled={ isLoading }>Login</button>
</form>
</>
);
}
export default Auth;
If you want to read more about react-query
, here is the official documentation.
In this part of the experiment, we will be using Sendinblue
to send emails. If you don't have account in 'Sendinblue', you may create your own in this link.
In this part of the experiment, we will be using Sendinblue
to send emails. If you don't have an account at 'Sendinblue', you may create one at this link.
Once you are done creating your own account, you should see this page:
Now, click Transactional
and then Settings
in the left navigation bar. You should be able to see this page:
Once you are there, click Configuration
and Get Your SMTP Key.
You should be on this page.
Now, click Generate a new SMTP key.
A modal should appear, and you may enter any key you want. In my case, I use lab_exp
. Then click the Generate
button.
After you click the Generate
button, the SMTP key of Sendinblue should appear; copy that and don't lose it.
To set up SMTP in PocketBase
, go to the admin UI home page. Then click the setting icon and click on Mail Settings
. You should be able to see this page:
Now, toggle Use SMTP mail server (recommended)
. A user interface for configuring your SMTP mail server would appear. Remember the SMTP key you copied earlier? You'll be pasting that in the password
field.
For the username field, go back to Sendinblue and close the modal for the SMTP key. Copy the login
:
For the SMTP Server Host
field, paste smtp-relay.sendinblue.com
, which can be seen back on the Sendinblue page. Then you may now click Save changes
.
Send test email
will appear, and when you click that, you can test if the configuration you made is working.
This is an optional part of the documentation in case you encountered an error while setting up the mail settings of PocketBase
.
During the experiment at the lab, I encounter this error:
To resolve this issue, delete the following files:
CHANGELOG.md
LICENSE.md
pocketbase.exe
- pb_data
Since you deleted the pb_data
. You have to repeat the configuration of PocketBase mail settings and create an admin account for the Admin UI. You may read the Dangers of Deleting pb_data included in this documentation.
In PocketBase
, there are two types of collections:
- Base Collection - is the default collection type, and you can use it for any type of data.
- Auth Collection - contains extra fields to manage users, like username, email, and verified. An example of an
Auth collection
is the PocketBase default user collections.
This was discussed because later we will be creating a simple way of authenticating users.
In getting the records into PocketBase, here is an example from the official documentation:
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
...
const record1 = await pb.collection('posts').getOne('RECORD_ID', {
expand: 'relField1,relField2.subRelField',
});
This is how we implement it in hooks/UseVerified.js
:
const user_id = PB.authStore.model.id;
const user_data = await PB.collection("users").getOne(user_id);
setIsVerified(user_data.verified);
To start off, we need the following:
- First, a button that will send email verification
- Then, a function will handle the click event for sending email verification.
- Lastly, an indicator of whether the user is verified or not
Earlier, UseVerified.js
was mentioned; add the following function to that file:
async function requestVerification(){
const email = PB.authStore.model.email;
const response = await PB.collection("users").requestVerification(email);
if(response){
alert("Verification email sent! Check your inbox.");
}
}
Now, we're done with the function that will handle the sending of emails. In Auth.js
, import the UseVerified
custom hook:
import UseVerified from "hooks/UseVerified";
...
const { is_verified, requestVerification } = UseVerified();
Now, create the indicator and the button in Auth.js
:
<p>Verified: { is_verified.toString() }</p>
{ !is_verified && <button onClick={requestVerification}>Send Verification</button> }
Your UseVerified.js
should look like the following:
import { useState, useEffect } from "react";
import PB from "lib/pocketbase";
function UseVerified(){
const [ is_verified, setIsVerified ] = useState(false);
useEffect(() => {
async function checkVerified(){
const user_id = PB.authStore.model.id;
const user_data = await PB.collection("users").getOne(user_id);
console.log("Fetch data: ", user_data);
setIsVerified(user_data.verified);
}
const is_logged_in = PB.authStore.isValid;
/* Invoke checkVerified() function if the user is logged in */
if(is_logged_in){
checkVerified();
}
}, []);
async function requestVerification(){
const email = PB.authStore.model.email;
const response = await PB.collection("users").requestVerification(email);
if(response){
alert("Verification email sent! Check your inbox.");
}
}
return { is_verified, requestVerification };
}
export default UseVerified;
In testing it, you should use your email account, and you should be able to receive this email once you test it:
Click the Verify
button in the email, return to your React app, and refresh it. Verified
should have a value of true
now.
In this chapter of the documentation, we will be making changes to the app from the previous chapter and enabling it to reflect the changes that are happening in the database (PocketBase).
In order to do this, we will be using useQuery
in React, and you may visit the official documentation by clicking this.
There will be a huge changes in UseVerified.js
, first import the useQuery
:
There will be huge changes in UseVerified.js
. First, import useQuery
.
import { useQuery } from "react-query";
Then UseVerified()
will have a different return value:
return useQuery({
queryFn: checkVerified,
queryKey: [ "check-verified", user_id ]
});
This is the last chapter in this documentation; you can click this to see the whole change in UseVerified.js
. There were minor changes in Auth.js
as well; you can click [this](https://github.com/MadrinanComLab/Exp-PocketBase/blob/master/pb_app/src/components/Auth.js to see the changes.
Keys in the option of useQuery
was case sensitive. For example, you cannot make queryFn into queryFunction or other keys that you may wish to use.
The object that is used as an argument is called options
, and each of its attributes has a different purpose:
queryFn
- The function you will assign here will be responsible for fetching the data you need.
Note: Since checkVerified
was used to be the value of queryFunction
, the return value of it will change:
return user_data.verified
queryKey
- This will be used for data caching, and the value of"check-verified"
will be the key, anduser_id
is the second argument, which in this case will be used to query the user by its record id.
Note: The value of user_id
will come from the code snippet below. Remember, this will have a value when you log in a user, then it becomes undefined
when no user is logged in, and that is why optional chaining was implemented.
...
export default function UseVerified(){
const user_id = PB.authStore.model?.id;
...
}
Stay tuned for upcoming projects and experiments by following me on the following accounts: