SvelteKit serves as our chosen frontend framework or “BFF” (Backend For Frontend). This powerful framework facilitates seamless development of efficient and interactive user interfaces. Leveraging its component-based architecture, SvelteKit allows us to create dynamic web applications with minimal boilerplate code.
Pocketbase plays a crucial role as a single, compact binary file serving as our “BaaS” (Backend as a Service) for the BFF. This innovative solution simplifies backend operations by providing essential functionalities without the need for extensive infrastructure setup. With Pocketbase, we can efficiently manage databases, authentication, and other backend services, allowing us to focus more on building engaging user experiences.
Vercel stands out as our preferred platform for deploying our landing page. Its zero-config requirement for SvelteKit web apps streamlines the deployment process, allowing us to effortlessly showcase our application to the world. Additionally, Vercel’s reliability and scalability make it an excellent choice for hosting our landing page. While Vercel handles the production environment, we also implement a redundant deployment strategy by hosting a version on our homelab, ensuring flexibility and control over our deployment infrastructure.
Our main UI Toolkit, allows us to create accessible interface, with it comes one of its dependency which is floating-ui/dom and tailwindcss. floating-ui/dom is only required if you need the Popup elements, given that tailwind is required by skeleton components extends tailwindscss utility-first classes.
Installation steps are as follows after creating a fresh sveltekit project, you just need to install skeleton , tailwind and floating-ui/dom with the following commands
Installing Skeleton
npm install -D @skeletonlabs/skeleton @skeletonlabs/tw-plugin #Requirednpm install @floating-ui/dom #Not required if not using any of popup
Install Tailwind with the following command
npx svelte-add@latest tailwindcss --tailwindcss-forms --tailwindcss-typographynpm install
svelte-add is a handy tool to add more integrations in a svelte or sveltekit project, also supports bootstrap, bulma,mdsvex, imagetools and more! check them out too!
after installing all of that skeleton requires you to modify the tailwind configuration which is located at the root of your project tailwind.config.[ts|js|cjs]
we used typescript in our project so we use the .ts
file extension here is what our configuration looked like!
import { join } from 'path' // Required when using typescript, as we used join blowimport type { Config } from 'tailwindcss'import forms from '@tailwindcss/forms'import typography from '@tailwindcss/typography'//Skeleton Specific importsimport { skeleton } from '@skeletonlabs/tw-plugin'import { daedalus } from './src/themes'
const config = { // Dark mode handled via the class method darkMode: 'class', content: [ //Tailwind defaults './src/**/*.{html,js,svelte,ts}', // Skeleton needs to append this because their components rely on utility classes join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}') ], theme: { extend: {} }, plugins: [ forms, typography, skeleton({ themes: { // Our generated theme that aligns with skeleton's design token custom: [daedalus] } }) ]} satisfies Config
export default config
The generated theme source code can be viewed here, generated our theme here.
To enhance our developer experience, we introduced additional dependencies, including:
As recommended by Skeleton, learn more about its usage here.
tailwind-merge and clsx:
import { type ClassValue, clsx } from 'clsx'import { twMerge } from 'tailwind-merge'
export const sx = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
Now, you can employ the sx function in your Svelte files like this:
class={sx('input-bordered input w-full', errors ? 'input-error' : '')}
Check out the actual implementation here
These two proved invaluable for dynamically injecting classes into our components based on a given state. This implementation provides a convenient and efficient way to manage dynamic class injections, improving the flexibility and maintainability of our codebase.
We’ve incorporated Skeleton’s layouting component, [<AppShell />](https://www.skeleton.dev/components/app-shell)
into our design. This integration necessitated updates to both app.html
and app.postcss
. The final output for these files now resembles the following, Feel free to explore the provided link for a deeper understanding of the features and capabilities offered by Skeleton’s AppShell component.
app.html
<!doctype html><html lang="en" class="dark"> <head> <meta charset="utf-8" /> <link rel="icon" href="%sveltekit.assets%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> %sveltekit.head% </head> <body data-sveltekit-preload-data="hover" data-theme="daedalus"> <div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div> </body></html>
app.postcss
/* Write your global styles here, in PostCSS syntax */@tailwind base;@tailwind components;@tailwind utilities;@tailwind variants;
html,body { /* Installed via fontsource */ font-family: 'Inter Variable', sans-serif; @apply h-full overflow-hidden;}
Expanding on our root layout, we’ve introduced some significant enhancements. In this updated layout, we incorporate the Inter font and app.postcss
Furthermore, we initialize the @floating-ui/dom utilities using stores provided by skeleton. Additionally, we initiate the use of sveltekit-view-transition by @paoloricciuti.
The inclusion of Skeleton adds a layer of utility components and global stores that we initialize at this stage, making them accessible across our various components and pages. Noteworthy additions include:
For detailed information on these utilities, refer to the Skeleton documentation. This refined root layout sets the stage for a more sophisticated and feature-rich user interface throughout our application.
<script lang="ts"> import '@fontsource-variable/inter'; import '../app.postcss'; import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom'; import { Modal, Toast, storePopup } from '@skeletonlabs/skeleton'; import { initializeStores } from '@skeletonlabs/skeleton'; import { setupViewTransition } from 'sveltekit-view-transition'; import { ModalRegistry, Drawers } from '@components'; import { onNavigate } from '$app/navigation';
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow }); initializeStores(); setupViewTransition();
// Use onNavigate instead of afterNavigate so it will work on url fragment identifier eg #about onNavigate(() => { let elemPage: HTMLElement | null = document.querySelector('#page'); elemPage!.scrollTop = 0; });</script>
<Drawers /><Modal components={ModalRegistry} /><Toast position="br" /><slot />
We’ve implemented group, a remarkable feature that empowers us to break free from a uniform layout structure. This allows us to employ distinct layouts for specific pages throughout our application, providing a versatile and tailored user experience.
Always prioritize security by validating user inputs and never solely rely on client-side data. This crucial practice is exemplified through the use of two powerful tools: sveltekit-superforms created by sveltekit-superforms created by ciscoheat and zod created by collinhacks. These libraries work in tandem to reinforce the integrity of user input, ensuring a robust and reliable validation process.
Zod Usage
export const requiredString = ( name: string, constraint?: { min?: number max?: number }) => { if (constraint?.min && constraint?.max) { return string({ required_error: `${name} is required.` }) .min(constraint.min, `${name} must contain atleast ${constraint.min} characters.`) .max(constraint.max, `${name} must contain atleast ${constraint.max} characters.`) } else if (constraint?.min) { return string({ required_error: `${name} is required.` }).min(constraint.min, `${name} must contain atleast ${constraint.min} characters.`) } else if (constraint?.max) { return string({ required_error: `${name} is required.` }).max(constraint.max, `${name} must contain atleast ${constraint.max} characters.`) } else { return string({ required_error: `${name} is required.` }) }}
export const userRole = zEnum(['admin', 'team', 'user'])
export type UserRole = zInfer<typeof userRole>
export const usernameSchema = requiredString('Username', { min: 3, max: 16 }) .trim() .toLowerCase() .regex(/^[a-zA-Z0-9_]+$/)
// Actual schema at pocketbaseexport const userSchema = object({ id: string(), username: usernameSchema, firstName: requiredString('First Name', { min: 3, max: 32 }), lastName: requiredString('Last Name', { min: 3, max: 32 }), avatar: string(), email: string({ required_error: 'Email address is required' }).email('Invalid email address'), verified: boolean(), role: userRole.default('user'), created: string().datetime(), updated: string().datetime()})
// this is for superforms create and updateexport const userFormSchema = userSchema .omit({ created: true, updated: true }) .extend({ id: userSchema.shape.id.optional(), role: userSchema.shape.role.optional(), verified: userSchema.shape.verified.optional(), avatar: userSchema.shape.avatar.optional() })
Zod stands out due to its versatility and adaptability. For instance, in working with the requiredString type, I found that I could leverage Zod’s built-in methods to generate accurate error messages based on the specified field name. This capability proves particularly useful in scenarios involving fields such as firstName and lastName. Additionally, I extended this functionality to the username field by incorporating actions like trimming, converting to lowercase, and implementing a regex pattern to ensure field validity. It’s a niche feature that enhances the overall flexibility and customization provided by Zod.
auth.type.ts login-form.svelte
<script lang="ts"> import { page } from '$app/stores'; import { loginSchema } from '@types'; import { superForm } from 'sveltekit-superforms/client'; import TextInput from './text-input.svelte'; import PasswordInput from './password-input.svelte'; import { getToastStore } from '@skeletonlabs/skeleton'; import { ASSET_URL } from '@utils'; // import Icon from '@iconify/svelte'; const toast = getToastStore(); const { form, errors, constraints, enhance, message } = superForm($page.data.form, { validators: loginSchema, onResult: async ({ result }) => { if (result.type === 'redirect') toast.trigger({ message: 'Login successful' }); }, applyAction: true, invalidateAll: true });</script>
<div class="flex flex-col gap-4"> <div class="mx-auto py-4"> <img src={`${ASSET_URL}logo.png`} alt="Daedalus Logo" /> <h1 class="h2">Login</h1> </div> <div class="form-control mx-auto min-w-[50%] text-center"> {#if $message} <div class="variant-ghost-error p-4">{$message}</div> {/if} </div> <form method="POST" action="/api/actions/auth?/legacy" class="form mx-auto min-w-[50%]" use:enhance > <TextInput name="key" label="Username or Email" placeholder="e.g johnwick" bind:value={$form.key} errors={$errors.key} constraints={$constraints.key} /> <PasswordInput name="password" label="Password" placeholder="Password" bind:value={$form.password} errors={$errors.password} constraints={$constraints.password} /> <button class="variant-filled-primary btn my-4 w-full">Login</button> </form> <p class="text-center"> No account yet? Register <a class="underline" href="/register">here</a>. </p></div>
export const actions: Actions = {legacy: async ({ request, locals }) => { // Sperforms using the zod schema we wrote at auth.types.ts const form = await superValidate(request, loginSchema); // This returns field errors, values and an additional message if (!form.valid) return message(form, 'Please fill in all required fields'); // extract the information from the form const { key, password } = form.data;
try { // Authenticate the user await locals.DB.collection(Collections.Users).authWithPassword(key, password); } catch (error) { // if something goes wrong we can return an error message depending on the response from our pocketbase api const err = error as ClientResponseError; return err.response.code !== 400 ? message(form, INVALID_CREDENTIALS) : message(form, SOMETHING_WENT_WRONG, { status: err.response.code }); } // Redirect the user if successful if (locals.DB.authStore.isValid && locals.DB.authStore.model) { locals.user = locals.DB.authStore.model; redirect(302, '/me'); } // This is safeguard for superforms always return the form, incase all of the above conditions aren't met // this one is returned. return message(form, `Bad Request`); },};
Carta is a new lightweight, fast and extensible markdown editor created by BearToCode, works like a charm in SvelteKit bonus is that it supports Server Side Rendering(more on this later) check out more of their examples here
We used most of the available plugins of carta available e.g Attachment, Code, emoji and slash. where most of them are useful in editing our profile page.
Carta plugins specially the attachment part works well with our pocketbase api, where it needs an endpoint that returns a url for the uploaded attachment.
import { error, json } from '@sveltejs/kit'import type { RequestHandler } from './$types'import { db } from '@server'import { Collections } from '@types'import type { ClientResponseError } from 'pocketbase'
// Post endpoint at "/api/attachtmenths"export const POST: RequestHandler = async ({ request, locals }) => { const formData = await request.formData() const file = formData.get('file') // validate if file from form data is instance of a file // and check if user is authenticated to do the request if (file instanceof File && locals.user) { try { // Upload the attachment const upload = await db.collection(Collections.Media).create({ file }) // return a json with url return json({ url: db.files.getUrl(upload, upload.file) }) } catch (e) { // Return the actual error from pocketbase const err = e as ClientResponseError error(400, { message: err.message }) } } // just a safeguard error(400, { message: 'No file provided' })}
const carta = new Carta({ sanitizer: purifier.sanitize, extensions: [ attachment({ async upload(file) { const formData = new FormData() formData.append('file', file) const response = await fetch('/api/actions/attachments', { method: 'POST', body: formData }) .then((res) => res.json()) .then((res) => res.url as string) return response } }), emoji(), slash(), code() ]})
A featured profile can be previewed at the given link, a co member of the community straightly copied his README.md at github although its not perfectly aligned(we will fix it in future revisions) you have to admire how simple it is implemented on sveltekit and carta. code snippets are located at the pocketbase section.
SvelteKit seamlessly integrates with various APIs, but when it comes to a unique solution, Pocketbase stands out. What sets Pocketbase apart is its distinctive blend of a Svelte-built admin UI, backed by Go. This choice was deliberate, driven by the rich features it brings to our stack, including:
Upon initial inspection, one might question the readiness of pocketbase created by @ganigeorgiev for production use, it is not! he created it intentionally for his other project presentator a design presentation and collaboration platform. One noteworthy aspect is that Pocketbase, designed intentionally as a single binary with SQLite embedded underneath, allows us to opt for breaking changes that align with the version of the SDK we use. Presently, we are utilizing v.20.0, deployed at pockethost an open source project created by @benallfree. Pockethost serves as a cloud hosting platform tailored for Pocketbase, contributing to the robustness and scalability of our chosen stack.
We also hooked our pocketbase file storage to Cloudflare R2 a global object storage by cloudflare with 10gb free storage! all our media files are stored there even the ones that come from carta attachment plugin.
Incorporating the Pocketbase JavaScript SDK, we initiated a global server only instance using the following setup: Check out the source code here
import { env as publicEnv } from '$env/dynamic/public'import { env } from '$env/dynamic/private'import PocketBase from 'pocketbase'
// Initialize a Pocketbase client with authenticationexport const createClient = async () => { const pocketbase = new PocketBase(publicEnv.PUBLIC_PB_URL) try { // Authenticate as admin using provided credentials await pocketbase.admins.authWithPassword(env.PB_ADMIN_EMAIL!, env.PB_ADMIN_PASSWORD!) } catch (e) { console.log(e) } return pocketbase}
// Create a global instance of the Pocketbase clientexport const db = await createClient()// Disable auto-cancellation to prevent unintended termination of operationsdb.autoCancellation(false)
This module, strategically placed as a Server-only module ensures that sensitive information such as credentials is not bundled with the client-side code. This is a security measure provided by SvelteKit, allowing us to safeguard our secrets and maintain a secure development environment.
Usage of the global instance, we can use the pocketbase instance directly on our server side code but we went away and further abstracted the queries
import { db } from '@server'
import type { RecordModel } from 'pocketbase'
enum Collections { Users = 'users', UsersDetails = 'users_details'}
// Query user by idexport const queryUser = (userId: string) => db .collection(Collections.Users) .getOne(userId) .then((data) => { // pocketbase by default returns the fileID, we need to get its actual URL so we can use this directly // in an <img /> tag data.avatar = db.files.getUrl(data, data.avatar) return data }) .catch(() => { return undefined })// Querying by fields with typesafetyexport const queryUserByUsername = async (username: string): Promise<RecordModel | undefined> => { // Safeguard query we ensure that the user exists const query = await db .collection(Collections.Users) // the sdk provides us with a `db.filter` with the following syntax .getFirstListItem(db.filter('username = {:username}', { username })) .catch(() => undefined) if (!query) return undefined // Querying the actual User Details return db .collection(Collections.UsersDetails) .getFirstListItem(db.filter('user= {:id}', { id: query.id }), { expand: 'user' }) .then((data) => { if (data.expand?.user?.avatar) // Same as above as the avatar file is stored on the original User table used for only for authentication and identification, // we also added a parameter in our query to get only the thumbnail of the file data.expand.user.avatar = db.files.getUrl(data.expand.user, data.expand.user.avatar, { thumb: '300x350' }) return data }) .catch(() => undefined)}
src/routes/(profile)/[username]/+page.server.ts
import { error } from '@sveltejs/kit'import type { PageServerLoad } from './$types'import { queryUserByUsername } from '@server/queries'
export const load: PageServerLoad = async ({ params }) => { // Extract the username from url [username] const { username } = params if (username === 'admin') error(404, `Not found`) // Query user from pocketbase const query = await queryUserByUsername(username) // Return an error page if user with given username does not exist if (!query) error(404, `Not found`) // Return the object return { query }}
Rendering the data
Using Carta as renderer
<script lang="ts"> import { UserCard } from '@components'; import type { PageData } from './$types'; import { CartaViewer, Carta } from 'carta-md'; import { formatDistance } from 'date-fns'; import { code } from '@cartamd/plugin-code'; import { emoji } from '@cartamd/plugin-emoji'; import { ASSET_URL } from '@utils';
// Get data from page load this can be PageServerData or PageData export let data: PageData;
// Destructure the query returned from page load and rename it as user const { query: user } = data;
//Initializing carta on this page with the plugins we need const carta = new Carta({ extensions: [code(), emoji()] });
</script>
<svelte:head> <title>{`Daedalus - ` + user.expand?.user.username}</title> <meta property="og:type" content="article" /> <meta property="og:title" content={user.expand?.user.username} /> <meta property="og:description" content={user.bio} /> <meta property="og:image" content={user.expand?.user.avatar ? user.expand.user.avatar : ASSET_URL + 'daedalus.png'} /></svelte:head>
<div class="m-8 flex h-screen w-screen"> <div class="basis-1/4"> <UserCard {user} /> <p class="py-4 text-center text-xl"> <span class="font-bold tracking-wide text-secondary-500"> Joined </span> <span class="font-medium text-tertiary-900"> {formatDistance(new Date(user.created), new Date(), { addSuffix: true })} </span> </p> </div> <div class="basis-3/4"> <CartaViewer {carta} value={user.details} /> </div></div>
An example of how we utilize pocketbase sdk and sveltekit’s capability an example here.
Pocketbase Filters and API Rules
export type eventFilter = { query?: string sort?: string}
export const queryEvents = ( // set defaults for parameters page: number = 1, perPage: number = 10, filter: eventFilter = { query: '', sort: 'created' }) => db .collection(Collections.Events) .getList(page, perPage, { // sort: '-date', // Not needed as we already disabled autocancellation requestKey: 'events', filter: db.filter( '(title ?~ {:title} || description ?~ {:description} || details ?~ {:details})', { title: filter.query, description: filter.query, details: filter.query } ) }) .then((collection) => { const events = collection.items.map((event) => { // Replace the preview property with actual file url! event.preview = db.files.getUrl(event, event.preview) return event }) collection.items = events return collection })
src/routes/(default)/events/+page.server.ts
import type { PageServerLoad } from './$types'import { queryEvents } from '@server/queries'
export const load: PageServerLoad = ({ url }) => { // extract search params with key "q" or default with "" const query = url.searchParams.get('q') ?? '' // appends the page and perpage query provided by the EventPaginator Component const page = Number(url.searchParams.get('page')) + 1 || 1 const perPage = Number(url.searchParams.get('perPage')) || 6
return { // Return events as streamed promise events: queryEvents(page, perPage, { query }) }}
src/routes/(default)/events/+page.svelte
<script lang="ts"> import { ASSET_URL } from '@utils'; import type { PageData } from './$types'; import EventPaginator from './event-paginator.svelte'; import { EventCard } from '@components'; export let data: PageData;</script>
<EventPaginator />
{#await data.events} Loading..{:then events} <div class="grid grid-cols-1 gap-8 pt-4 md:grid-cols-3"> {#each events.items as event} <EventCard {event} /> {/each} </div>{:catch} No events{/await}
Svelte includes a logic block #await, allowing us to branch on Promise states, pending, fulfilled or rejected. this is comparable to jsx’s(react, solidjs) or vue(experimental) <Suspense />
component.
Most of the code implementation is at EventPaginator
component where the input on the search is debounced and appended on the url as query parameters, it searches for the events collection for keywords of the desired event , given that the keywords exist on events.
In the sections aboved, i have written about us utilizing zod binded in our forms and data types from pocketbase to forms, in all of our forms we used the best feature i think that svelte has to offer which is Form Actions,
src/routes/api/actions/users/+page.server.ts
api endpoint and the userdetails-form.svelte, +page.server.ts where we load the data to the page.
export const actions: Actions = { details: async ({ request, locals }) => { // Validate form using zod schema const form = await superValidate(request, userDetailsFormSchema) // return the form data submitted with errors if invalid if (!form.valid) return fail(400, { form })
try { // Check if user is authenticated if (locals.user) { // extract user id const { id } = locals.user // Update data base on the fields const details = await db .collection(Collections.UsersDetails) .getFirstListItem(`user="${id}"`) await locals.DB.collection(Collections.UsersDetails).update(details.id, { bio: form.data.bio || '', details: form.data.details || '', x: form.data.x || '', linkedin: form.data.linkedin || '', github: form.data.github || '', user: id, updated: new Date() }) } // as per superforms always return the form data! return { form } } catch (error) { // Cast error as ClientResponseError from pocketbase SDK const err = error as ClientResponseError // as per superforms always return the form data but now with a custom message return err.response.code !== 400 ? message(form, INVALID_CREDENTIALS) : message(form, err.message, { status: err.response.code }) } }}
The superform/client
module allows us to provide the form data and bind the value using the bind:value
directive from svelte, including the erros and constraints defined in the schema we wrote using zod.
<script lanmg="ts"> import { userDetailsFormSchema } from '@types'; import { superForm } from 'sveltekit-superforms/client';
const { form, errors, constraints, enhance, message, delayed, tainted } = superForm( $page.data.form, { validators: userDetailsFormSchema, onResult: async ({ result }) => { if (result.type === 'success') toast.trigger({ message: 'Profile updated.' }); } } );</script><form method="POST" action="/api/actions/users?/details" class="form mx-auto min-w-[50%]" use:enhance ><TextInput name="bio" label="bio" placeholder="low born noob" bind:value={$form.bio} errors={$errors.bio} constraints={$constraints.bio} />.... more snippet here <button class="variant-filled-primary btn my-4 w-full" disabled={$delayed || !$tainted} >Update</button ></form>
We can also write the endpoint in the same +page.server.ts
on the same route but we prefer to declare all our action endpoints at the /api/actions
route, form-action
api requires method="POST"
if you use method="post"
it will not work. use:enhance
is the key to making the form actions work smoothly, hydrating the data based on the page load and returned data from the action endpoint.
Pocketbase Authentication uses JWT, by default admin requests can access its entire API, that is why our global instance is used as server-only-module
, with sveltekit server hooks think of it like a middleware or an interceptor, in this case it intercepts all the requests as we have not filtered on a per route basis.
src/hooks.server.ts and src/app.d.ts
import type { Handle } from '@sveltejs/kit'import PocketBase from 'pocketbase'import { env } from '$env/dynamic/public'import { Collections } from '@types'
export const handle: Handle = async ({ event, resolve }) => { // Initialize a client side pocketbase instance const USER_DB = new PocketBase(env.PUBLIC_PB_URL) // assign it to locals event.locals.DB = USER_DB
// Check for cookie on if it exists on requests event.locals.DB.authStore.loadFromCookie(event.request.headers.get('cookie') || '')
try { // Refresh the cookie if it is valid event.locals.DB.authStore.isValid && (await event.locals.DB.collection(Collections.Users).authRefresh()) // Assign the details to locals.user event.locals.user = { ...event.locals.DB.authStore.model } } catch (error) { event.locals.DB.authStore.clear() } // Resolve the request const response = await resolve(event) // set the coookie response.headers.set('set-cookie', event.locals.DB.authStore.exportToCookie()) // return response return response}
Locals can only be accessed by the server side modules e.g *.server.{ts|js}
files, in our root(default) +layout+server.ts
we have the following
import { redirect, error } from '@sveltejs/kit'import type { LayoutServerLoad } from './$types'import { queryUser } from '@server/queries'
export const load: LayoutServerLoad = async ({ locals }) => { if (!locals.DB.authStore.isValid && !locals.user) error(401, 'Unauthorized') if (!locals.user) { redirect(307, '/login') } else { return { user: await queryUser(locals.user.id) } }}
The following implementation is not safe there is an existing open issue at github about how to safely implement it on child routes check issue here, we need to await
the parent data on child routes. e.g:
import { redirect } from '@sveltejs/kit'import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ parent }) => { // await parent data just to revalidate the layout load const { user } = await parent() // safeguard if user does not exist redirect if (!user) redirect(307, '/login') // return the awaited data from layout load return { user }}
On all our server action that needs authentication we also access the locals.user
context which allows us to easily grab the user’s identity on a secured route / action.
Pocketbase migration is easy as pasting the json file from development to production. check out the files here, it also supports backing up and importing data in its admin ui.
We did not need to instal lthe vercel adapter as we didn’t feel the need to use most of what their platform offers, e.g analytics, db, kv etc …
Vercel is as straightforward as other cloud platforms we only need to link the repository, set the domains and environment variables, we can do further by auto previews on each PR made, but our repository is on an organization the actual linked repository is at one of our personal github accounts.
Download pocketbase runtime depending on your OS. paste it on the pb
directory;
You don’t need to touch that directory.
After downloading try running pnpm run dev:api
If successful initialize your instance by going at http://localhost:8090/_/
Copy the .env.example
to .env.development
cp .env.example .env.development
Whatever you put in the installer setup should also be in your .env.development
credentials
Use the sample data just upload the sampledata.zip
located at pb
directory to your local pb instance at Backups
You local data that you have will stay with you as pb/pb_data
is gitignored
This will keep our production data pristine
You can now install node dependencies using pnpm install
On a seperate terminal run the ff commands on each one pnpm dev:api
and pnpm dev:web
In case you are lazy run both using pnpm dev
but some systems might not support closing each child process.
Contribute and make a PR.
For further questions reachout with the team
todo: self hosting guide