@hfu.digital/loopkit-nestjs
v2026.4.2
Published
Production-ready flashcard engine for NestJS — SM-2 SRS algorithm, Prisma adapter, content pipeline
Readme
@hfu.digital/loopkit-nestjs
Production-ready flashcard engine for NestJS applications. Implements the SM-2 spaced repetition algorithm with a pluggable storage layer.
Installation
bun add @hfu.digital/loopkit-nestjsPrisma Schema
Copy these models into your schema.prisma:
model LoopKitCard {
id String @id @default(uuid())
noteId String
templateId String
deckId String
state String @default("new")
easeFactor Float @default(2.5)
interval Float @default(0)
dueDate DateTime @default(now())
currentStep Int @default(0)
lapseCount Int @default(0)
reviewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
note LoopKitNote @relation(fields: [noteId], references: [id], onDelete: Cascade)
deck LoopKitDeck @relation(fields: [deckId], references: [id])
@@unique([noteId, templateId])
@@index([deckId, state])
@@index([deckId, dueDate])
}
model LoopKitNote {
id String @id @default(uuid())
noteTypeId String
fields Json
tags Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
noteType LoopKitNoteType @relation(fields: [noteTypeId], references: [id])
cards LoopKitCard[]
}
model LoopKitNoteType {
id String @id @default(uuid())
name String @unique
fields Json
templates Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notes LoopKitNote[]
}
model LoopKitDeck {
id String @id @default(uuid())
name String
description String?
parentDeckId String?
presetId String?
configOverrides Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent LoopKitDeck? @relation("DeckHierarchy", fields: [parentDeckId], references: [id])
children LoopKitDeck[] @relation("DeckHierarchy")
preset LoopKitDeckPreset? @relation(fields: [presetId], references: [id])
cards LoopKitCard[]
logs LoopKitReviewLog[]
}
model LoopKitDeckPreset {
id String @id @default(uuid())
name String
config Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
decks LoopKitDeck[]
}
model LoopKitReviewLog {
id String @id @default(uuid())
cardId String
deckId String
grade String
prevState Json
newState Json
reviewedAt DateTime @default(now())
timeTakenMs Int
deck LoopKitDeck @relation(fields: [deckId], references: [id])
@@index([cardId])
@@index([deckId, reviewedAt])
}Then run prisma migrate dev.
Backend Integration
import { Module } from '@nestjs/common';
import { LoopKitModule, PrismaLoopKitAdapter } from '@hfu.digital/loopkit-nestjs';
import { PrismaService } from './prisma.service';
@Module({
imports: [
LoopKitModule.register({
storage: new PrismaLoopKitAdapter(prismaClient),
}),
],
})
export class AppModule {}Then inject services in your controllers:
import { ReviewSessionService, DeckService } from '@hfu.digital/loopkit-nestjs';
@Controller('loopkit')
export class FlashcardController {
constructor(
private readonly session: ReviewSessionService,
private readonly decks: DeckService,
) {}
}Exported Services
All services are automatically exported and injectable in any module that imports LoopKitModule:
ReviewSessionService— Build study queues and grade cardsDeckService— CRUD for decks with hierarchy supportNoteService— Create and manage study notesNoteTypeService— Define note types with field schemas and templatesImportExportService— CSV and JSON import/exportCardGenerator— Automatic card generation from notesLoopKitStorage— The storage adapter instance
Custom Storage Adapter
Implement LoopKitStorage for any database:
import { LoopKitStorage } from '@hfu.digital/loopkit-nestjs';
export class MyCustomAdapter extends LoopKitStorage {
// Implement all abstract methods
}Custom SRS Algorithm
Replace SM-2 with your own algorithm:
import { LoopKitModule, type SRSAlgorithm } from '@hfu.digital/loopkit-nestjs';
class FSRSAlgorithm implements SRSAlgorithm {
calculateNextState(card, grade, config, now) { /* ... */ }
getInitialState(config, now) { /* ... */ }
}
LoopKitModule.register({
storage: adapter,
algorithm: new FSRSAlgorithm(),
});Content Pipeline
Add custom transforms or use the built-in ones (requires peer dependencies):
import {
createContentPipeline,
createMarkdownTransform,
createKatexTransform,
} from '@hfu.digital/loopkit-nestjs';
import { marked } from 'marked';
import katex from 'katex';
const pipeline = createContentPipeline([
createMarkdownTransform(marked),
createKatexTransform(katex),
]);
LoopKitModule.register({ storage: adapter, contentPipeline: pipeline });License
MIT
