PotatoDB is a small TypeScript repository library for local development, tests, and hobby projects. It keeps application code domain-first while handling persistence behind the scenes.
bun add potato-dbPotatoDB is meant to feel small in application code. You construct a repository once for the current context, then work with domain entities and logical ids.
const userRepository = repositoryFactory.createUserRepository(request);
await userRepository.create({
displayName: "Ada",
role: "parent",
createdAt: new Date().toISOString(),
});
const users = await userRepository.findAll();
const ada = await userRepository.findByDisplayName("Ada");That is the main idea: application code stays focused on the domain operation, not on storage records, table names, or persistence wiring.
- Small repository classes with domain-specific methods.
- Logical ids instead of storage-level ids in application code.
- in-memory mode and json file-backed mode for different use cases, while keeping the same repository API
- Scope-aware repositories for tenant-owned data and application-wide data.
- Simple runtime sharing controls for normal app usage, isolated tests, or namespaced local runs.
PotatoDB separates repository access boundaries from runtime sharing.
ScopedRepositoryis bound to onescopeIdat construction time and only sees data for that scope. Perfect for multi-tenant applications with strict tenant boundaries.GlobalRepositoryis for application-wide shared data such as reference records.CrossScopeRepositoryis a query-only escape hatch for explicit reporting or directory-style reads across many scopes.
application: normal app behaviorisolated: private state per repository instance, mainly for testsnamespace: shared state within one named namespace, useful for parallel test runs or separate local apps
Omitting dataScope defaults to application.
Use mode: "json-file" when you want repository state to persist across
process runs.
const repository = new PublicHolidayRepository({
mode: "json-file",
dataScope: "namespace",
namespaceKey: "family-app",
});The repository API stays the same in file-backed mode. You still work with domain entities and logical ids, and PotatoDB handles persistence for you.
When you define a repository, you describe domain identity and let the base class handle persistence.
class UserRepository extends ScopedRepository<User, { userId: UserId }, UserCreateInput, FamilyId> {
constructor(familyId: FamilyId, options: PotatoDBOptions = {}) {
super({
typePrefix: "user",
scopeId: familyId,
createEntity: (createInput, generatedId) => ({
...createInput,
familyId,
userId: brandId(generatedId),
}),
idOf: (user) => ({ userId: user.userId }),
partitionKeyOf: () => familyId,
sortKeyOf: ({ userId }) => `User#${userId}#User`,
}, options);
}
}Keep repository construction and runtime configuration in one application-level factory so handlers do not need to know which request fields or options are required to build each repository.
const factory = new FamilyAppRepositoryFactory();
const request = {
authorizer: {
claims: {
familyId,
},
},
countryCode: "US",
date: "2026-04-18",
};
const userRepository = factory.createUserRepository(request);
const publicHolidayRepository = factory.createPublicHolidayRepository(request);That factory lives in this repository's example app and is not part of the published library API. It is included to demonstrate the recommended pattern in real application code.
Use put(...) when you already have a complete entity, such as during fixture
loading or test setup.
await repository.put({
holidayId: brandId<"HolidayId">("holiday-1"),
region: "US",
name: "Independence Day",
});For normal application code, prefer create(...) and update(...) so the
repository can own defaults, id generation, and entity-shape rules.