OpenLang Generative UI - Fenrir
Every question our business asks is legit; too bad we never build the dashboards they want...
· 10 min read
The combination of PocketBase and Angular is my personal favorite for fast prototyping and hobbying. What makes PocketBase stand out is how it manages to pack enterprise-grade features into a single executable while keeping the developer experience refreshingly simple. You get a complete backend solution that feels like it was built by developers who actually understand the pain points of modern web development.
Of all BaaS offerings, PocketBase is my favorite because it delivers everything you need without the typical BaaS headaches:
And Angular is my favorite because well... I've worked with it for years.
It's a great framework for building web applications. It continually evolves with new trends, like using Signals, Vite, and more. It has stood the test of time and is used a lot in enterprise applications.
Just to name a few:
Today we will dive into PocketBase and why I have been using it to build Tovedem, rather rapidly and carefree in combination with Angular.
So this blog will go over the basics of PocketBase, how to get started, how to use the JavaScript SDK, and how to use PocketBase with a typed service in Angular.
This combination of PocketBase and Angular has been a great experience and I have been able to build a lot of features quickly and easily. Definitely worth a try for quick (hobby-)projects or prototypes.
First off, what is PocketBase?
PocketBase is a backend service in a single file. It's being built as a framework in Go with a SQLite database backing. As said it is available to use as a framework, but not only that. It's also available as a database + backend application that encompasses most of the basic development work. Think of authentication (O-Auth), managing SQL tables with different relationships, APIs on this data, and real-time feeds.
Check out the PocketBase demo to get a feel for what PocketBase is capable of and what the UI looks like.
Another thing; The documentation is GREAT! While extending PocketBase functionality with custom Javascript endpoints, etc. the documentation has always been up-to-date and very searchable. The docs are easy to follow and understand, and breaking changes are well-documented and easy to follow.
Let's have a quick look at how to get started with PocketBase.
Self-host in your preferred way, either with Docker or simply by running the executable. Then visit localhost:8090/_/ to be visited by the dashboard. Within this dashboard, we can manage the authentication providers, database schema, the data inside, logging, and different kinds of settings.
Don't want to self-host? Use PocketHost for a managed PocketBase experience that keeps the zero-config feel without managing servers yourself. Of course you still get full access to all the features of PocketBase.
Managing your data schema is really easy and visual, just add the fields you want for a collection, and you're done! Notice how there are some complex types you can add easily like rich text editor texts, emails, and whole files. Also, take special mention of the 'relation' type which allows for configuring 1-n or n-1 or 1-1 relationships between different collections.
device comments collection. Notice how relational fields (like user and device) display readable names like "nergy101" instead of string IDs, making it much easier to work with your data in the admin interface. Once you have your PocketBase instance running, you can easily interact with it using the JavaScript SDK. Here's a quick example:
import PocketBase from "pocketbase";
// Initialize the client
const pb = new PocketBase("http://127.0.0.1:8090");
// Authenticate a user
const authData = await pb
.collection("users")
.authWithPassword("test@example.com", "1234567890");
// Create a new record
const data = {
title: "My First Post",
content: "Hello PocketBase!",
author: pb.authStore.model.id,
};
const record = await pb.collection("posts").create(data);
// Fetch records with filtering
const records = await pb.collection("posts").getList(1, 20, {
filter: 'created >= "2024-01-01"',
sort: "-created",
});
// Real-time subscription
pb.collection("posts").subscribe("*", function (e) {
console.log("Real-time update:", e.record);
});
Of course, for this to work, you need to create a collection named posts in the PocketBase instance with the fields title, content, and author. The id, created, updated fields are created automatically in every collection.
The SDK makes it incredibly simple to handle authentication, CRUD operations, and real-time updates with just a few lines of code.
While building Tovedem I wrapped the SDK in an Angular PocketbaseService that leans heavily on generics. Every helper (create, update, getAll, getPage, …) accepts a T type parameter, so TypeScript keeps the rest of my code perfectly aligned with the domain models, assuming those models mirror the PocketBase collections (or at least a subset of them). This buys us a few concrete wins:
RecordModel of PocketBase gets replaced with our own strongly typed models, so returned data is instantly usable and typed.Here's the relevant slice of the service (truncated for brevity) showing how every CRUD helper is typed:
import { inject, Injectable } from "@angular/core";
import PocketBase, { BaseModel } from "pocketbase";
import { Environment } from "@/environment";
import { Page } from "@/app/models/pocketbase/page.model";
type ListOptions = { expand?: string; filter?: string; sort?: string };
type GetOptions = { expand?: string };
@Injectable({ providedIn: "root" })
export class PocketbaseService {
private environment = inject(Environment);
private client = new PocketBase(this.environment.pocketbase.baseUrl);
async create<T>(collectionName: string, item: Partial<T>): Promise<T> {
const result = await this.client.collection(collectionName).create(item);
return result as T;
}
async update<T extends BaseModel>(collectionName: string, item: T): Promise<T> {
const result = await this.client
.collection(collectionName)
.update(item.id, item);
return result as T;
}
async getAll<T>(collectionName: string, options?: ListOptions): Promise<T[]> {
const data = await this.client
.collection(collectionName)
.getFullList(options);
return data as T[];
}
async getOne<T>(collectionName: string, id: string, options?: GetOptions): Promise<T> {
const data = await this.client
.collection(collectionName)
.getOne(id, options);
return data as T;
}
async getPage<T>(collectionName: string, page: number, perPage: number, options?: GetOptions): Promise<Page<T>> {
const list = await this.client
.collection(collectionName)
.getList(page, perPage, options);
return {
...list,
items: list.items as T[],
};
}
async delete(collectionName: string, id: string): Promise<boolean> {
const result = await this.client.collection(collectionName).delete(id);
return result;
}
}
The pagination helper leans on a tiny Page interface:
import { ListResult } from "pocketbase";
export interface Page<T> extends ListResult<T> {}
(Defined in @/app/models/pocketbase/page.model.ts)
Here's an example of a Group model, a small object that represents a group of users:
import { BaseModel } from "pocketbase";
export interface Group extends BaseModel {
name: string;
description?: string;
images?: string[];
}
And here's the typed CRUD flow wired into an Angular component, it's a bit illustrative for this example but it shows the power of the typed service and how it can be used to build a robust application.
import { Component, OnInit, inject, signal, computed } from "@angular/core";
import { PocketbaseService } from "@/app/shared/services/pocketbase.service";
import { Group } from "@/app/models/domain/group.model";
import { Page } from "@/app/models/pocketbase/page.model";
@Component({
selector: "app-group-admin",
templateUrl: "./group-admin.component.html",
})
export class GroupAdminComponent implements OnInit {
private readonly pocketbase = inject(PocketbaseService);
readonly groups = signal<Group[]>([]);
readonly selected = signal<Group | null>(null);
readonly page = signal<Page<Group> | null>(null);
readonly loading = signal(false);
readonly totalGroups = computed(
() => this.page()?.totalItems ?? this.groups().length
);
async ngOnInit(): Promise<void> {
await this.loadGroups();
}
async onGroupCreate(formValue: Pick<Group, "name" | "description">) {
const created = await this.pocketbase.create<Group>("groups", {
name: formValue.name,
description: formValue.description,
});
this.groups.update((list) => [...list, created]);
}
async onGroupUpdate(group: Group) {
const updated = await this.pocketbase.update<Group>("groups", group);
this.selected.set(updated);
await this.loadGroups();
}
async onGroupDelete(id: string) {
await this.pocketbase.delete("groups", id);
this.groups.update((list) => list.filter((g) => g.id !== id));
if (this.selected()?.id === id) {
this.selected.set(this.groups()[0] ?? null);
}
}
async onRefreshPage(pageNumber = 1) {
const nextPage = await this.pocketbase.getPage<Group>(
"groups",
pageNumber,
10,
{ expand: "members" }
);
this.page.set(nextPage);
}
async onSelect(id: string) {
const group = await this.pocketbase.getOne<Group>("groups", id, {
expand: "members",
});
this.selected.set(group);
}
private async loadGroups() {
this.loading.set(true);
try {
const result = await this.pocketbase.getAll<Group>("groups", {
expand: "members",
});
this.groups.set(result);
if (result.length) {
await this.onSelect(result[0].id);
}
await this.onRefreshPage();
} finally {
this.loading.set(false);
}
}
}
Because every method returns Promise of T (or Promise of Page of T in the pagination helpers), responses stay typed and the compiler points me to every place that needs updating whenever the schema evolves. It's a small abstraction, but it turned the PocketBase SDK into a first-class citizen inside Angular and made refactors far less scary.
Seperate note about performance: Does it scale?
The FAQ states:
Only on a single server, aka. vertical. Most of the time, you may not need the complexity of managing a fleet of machines and services just to run your backend. PocketBase could be a great choice for small and midsize applications - SaaS, mobile api backend, intranet, etc. Even without optimizations, PocketBase can easily serve 10 000+ persistent realtime connections on a cheap $4 Hetzner CAX11 VPS (2vCPU, 4GB RAM). You can find performance tests for various read&write operations in the official benchmarks repo . There is still room for improvements (I haven't done extensive profiling yet), but the current performance is already good enough for the type of applications PocketBase is intended for.
So why choose PocketBase over other BaaS options like Firebase, Supabase, or Appwrite?
The combination of PocketBase with a typed Angular service gives you several key advantages:
The typed service pattern I've shown here transforms PocketBase from a great BaaS into a first-class TypeScript citizen. When your schema changes, TypeScript will guide you to every place that needs updating, no more runtime surprises or manual grep searches through your codebase.
If you want more on PocketBase, be sure to sign up, follow or even contact me.
It's my plan to update this blog into a full tutorial and getting-started. Hope to see you around!
Cheers, and, by the way, "BaaS" translates to "boss" in Dutch.
Last updated: 2025-11-25