npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lpdjs/firestore-repo-service

v2.6.32

Published

⚡ Type-safe Firestore ORM with auto-generated repositories, advanced queries, relations with typed select, pagination with include, batch/bulk operations, aggregations and transactions.

Downloads

4,032

Readme

firestore-repo-service

Documentation npm version License

Type-safe Firestore repository layer with auto-generated query methods, CRUD, a Firestore→SQL sync pipeline via Pub/Sub + BigQuery, change-history tracking, and a file-based Hono HTTP server for Firebase Cloud Functions v2.

Full documentation at frs.lpdjs.fr

Installation

npm install @lpdjs/firestore-repo-service firebase-admin

Quick start

Define your models

interface UserModel {
  docId: string;
  email: string;
  name: string;
  age: number;
  isActive: boolean;
}

interface PostModel {
  docId: string;
  userId: string;
  title: string;
  status: "draft" | "published";
}

Create the repository mapping

import {
  createRepositoryConfig,
  buildRepositoryRelations,
  createRepositoryMapping,
} from "@lpdjs/firestore-repo-service";
import { doc } from "firebase/firestore";
import type { Firestore } from "firebase/firestore";

const repositoryMapping = {
  users: createRepositoryConfig<UserModel>()({
    path: "users",
    isGroup: false,
    foreignKeys: ["docId", "email"] as const,
    queryKeys: ["name", "isActive"] as const,
    refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
  }),
  posts: createRepositoryConfig<PostModel>()({
    path: "posts",
    isGroup: false,
    foreignKeys: ["docId", "userId"] as const,
    queryKeys: ["status"] as const,
    refCb: (db: Firestore, docId: string) => doc(db, "posts", docId),
  }),
};

// Optional: add relations
const mappingWithRelations = buildRepositoryRelations(repositoryMapping, {
  posts: {
    userId: { repo: "users", key: "docId", type: "one" as const },
  },
});

export const repos = createRepositoryMapping(() => db, mappingWithRelations);

Use the repositories

// Fetch a single document
const user = await repos.users.get.byDocId("user123");
const userByEmail = await repos.users.get.byEmail("[email protected]");

// Query multiple documents
const activeUsers = await repos.users.query.byIsActive(true);

// With query options
const results = await repos.users.query.byIsActive(true, {
  where: [["age", ">=", 18]],
  orderBy: [{ field: "name", direction: "asc" }],
  limit: 50,
});

// Populate a relation
const post = await repos.posts.get.byDocId("post123");
if (post) {
  const postWithUser = await repos.posts.populate(post, "userId");
  console.log(postWithUser.populated.users?.name); // type-safe
}

// Update
const updated = await repos.users.update("user123", {
  name: "New name",
  age: 31,
});

API reference

createRepositoryConfig()

| Option | Description | | ------------- | --------------------------------------------------- | | path | Collection path in Firestore | | isGroup | true for collection group, false for simple | | foreignKeys | Keys for get.by* methods (single document lookup) | | queryKeys | Keys for query.by* methods (multi-document query) | | refCb | Function that returns the document reference |

Sub-collection example:

comments: createRepositoryConfig<CommentModel>()({
  path: "comments",
  isGroup: true,
  foreignKeys: ["docId"] as const,
  queryKeys: ["postId", "userId"] as const,
  refCb: (db, postId, commentId) =>
    doc(db, "posts", postId, "comments", commentId),
});

Query options

interface QueryOptions<T> {
  where?: [keyof T, WhereFilterOp, any][]; // AND conditions
  orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
  orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
  limit?: number;
  offset?: number;
  select?: (keyof T)[];
  startAt?: DocumentSnapshot | any[];
  startAfter?: DocumentSnapshot | any[];
  endAt?: DocumentSnapshot | any[];
  endBefore?: DocumentSnapshot | any[];
}

CRUD

// Create (auto-generated ID)
const newUser = await repos.users.create({ email: "...", name: "...", age: 25, isActive: true });

// Set (create or replace)
await repos.users.set("user123", { ... });

// Set with merge
await repos.users.set("user123", { age: 31 }, { merge: true });

// Update (partial)
await repos.users.update("user123", { age: 32 });

// Delete
await repos.users.delete("user123");

// Document ref
const ref = repos.users.documentRef("user123");

// Raw collection ref
const colRef = repos.users.ref;

Batch & Bulk

// Atomic batch (max 500 operations)
const batch = repos.users.batch.create();
batch.set(repos.users.documentRef("u1"), { ... });
batch.update(repos.users.documentRef("u2"), { age: 25 });
batch.delete(repos.users.documentRef("u3"));
await batch.commit();

// Bulk (auto-split into batches of 500)
await repos.users.bulk.set([
  { docRef: repos.users.documentRef("u1"), data: { ... }, merge: true },
]);
await repos.users.bulk.update([...]);
await repos.users.bulk.delete([...]);

Real-time listeners

const unsubscribe = repos.users.query.onSnapshot(
  { where: [["isActive", "==", true]] },
  (users) => console.log(users),
);
unsubscribe();

Pagination

const firstPage = await repos.users.query.by({
  orderBy: [{ field: "createdAt", direction: "desc" }],
  limit: 10,
});

const nextPage = await repos.users.query.by({
  orderBy: [{ field: "createdAt", direction: "desc" }],
  startAfter: firstPage[firstPage.length - 1],
  limit: 10,
});

// Paginate with relations
const page = await repos.posts.query.paginate({
  pageSize: 10,
  include: [{ relation: "userId", select: ["docId", "name", "email"] }],
});

Aggregations

import { count, sum, average } from "@lpdjs/firestore-repo-service";

const activeCount = await repos.users.aggregate.count({
  where: [["isActive", "==", true]],
});
const totalViews = await repos.posts.aggregate.sum("views");
const avgAge = await repos.users.aggregate.average("age");

Transactions

const result = await repos.users.transaction.run(async (txn) => {
  const user = await txn.get(repos.users.documentRef("user123"));
  if (user.exists()) {
    txn.update(repos.users.documentRef("user123"), {
      age: user.data().age + 1,
    });
  }
  return { success: true };
});

OR queries

// (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
const users = await repos.users.query.by({
  orWhere: [
    [
      ["status", "==", "active"],
      ["age", ">=", 18],
    ],
    [
      ["status", "==", "pending"],
      ["verified", "==", true],
    ],
  ],
});

Change History

Track every write (create / update / delete) to any Firestore document with zero-boilerplate Firestore triggers. Enabled per-repository via the createServers config.

const servers = createServers(repos, { onRequest, firestoreTriggers });

export const { historyTriggers } = servers.history({
  enabled: true,
  repos: { posts: true, users: true },
  // Optional: retention and relational tracking
  relational: true,
});

Each change is stored as an immutable snapshot in a __history sub-collection:

// Read history entries for a document
const entries = await repos.posts.history.byDocId("post123");

// Field-level lookup
const titleHistory = await repos.posts.history.byField("post123", "title");

Full documentation: frs.lpdjs.fr/guide/history


Hono File-Based API Server

A typed, file-based HTTP server built on Hono, designed to ship one Firebase Cloud Function v2 per logical API — with auto-generated OpenAPI 3.1, Zod validation, and a CLI scaffolder.

Install extras

npm i hono @hono/node-server
npm i -D @asteasolutions/zod-to-openapi

Bootstrap

npx frs init          # interactive — creates apis.ts + manifest stub + .frsrc.json
npx frs new createPost --domain posts --method post --api v1
npx frs gen --root src/domains   # refresh manifest (run before each build)

frs init also writes a .frsrc.json (project root, e.g. root, apisFile, apis). Later commands read it so flags become optional — precedence is flag → .frsrc.json → default. See the Hono guide for the full key list.

Configure your APIs (apis.ts)

import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";

export const apis = createApiRegistry({
  v1: {
    basePath: "/v1",
    openapi: { info: { title: "Public API", version: "1.0.0" } },
    interceptor: async ({ next, c }) => {
      const data = await next();
      return c.json({ success: true, data, error: null });
    },
    verbose: process.env["NODE_ENV"] !== "production",
  },
});

export const defineRoute = apis.defineRoute;
export const useCaseRoute = apis.useCaseRoute;

Write a route

A useCase owns its Zod input / output schemas (as static members) and the business logic; useCaseRoute wires it into an endpoint in one line.

// src/domains/posts/useCases/createPost/useCase.ts
import { z } from "zod";
import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
import type { Services } from "../../../../services.js";

const input = z.object({ title: z.string() });
const output = z.object({ id: z.string() });

export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
  static readonly input = input;
  static readonly output = output;

  async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
    return { id: payload.title };
  }
}
// src/domains/posts/useCases/createPost/routes.ts
import { defineRoutes } from "@lpdjs/firestore-repo-service/servers/hono";
import { useCaseRoute } from "../../../../apis.js";
import { CreatePostUseCase } from "./useCase.js";

export default defineRoutes([
  useCaseRoute(CreatePostUseCase, {
    api: "v1", // typed: only registered tags accepted
    method: "post",
    summary: "Create a post",
    tags: ["posts"],
  }),
]);

Need full control (no useCase)? Use defineRoute({...}) with an inline handler instead. Expose the same useCase under several APIs by adding more useCaseRoute(...) entries to the defineRoutes([...]) array.

Wire Cloud Functions

// src/index.ts
import { onRequest } from "firebase-functions/v2/https";
import { apis } from "./apis.js";
import { routes } from "./domains/__generated__/routes.js";

export const { v1 } = apis.toFunctions(routes, onRequest, {
  defaults: { region: "us-central1", invoker: "public" },
});

Key features

| Feature | Details | | ---------------------- | ------------------------------------------------------------------- | | File-based routing | routes.ts next to each useCase, scanned at build time | | Multi-API registry | One Cloud Function per tag, typed api field | | Zod validation | Body / query / path params + optional response validation | | OpenAPI 3.1 | Auto-generated from Zod schemas; Scalar UI at /docs | | Interceptor | Around-style hook for envelopes, error mapping, tracing | | Middlewares | Per-API and per-route Hono middlewares | | Typed context | Augment ContextVariableMap once, c.get("user") typed everywhere | | CLI | init / new (interactive) / gen |

Full documentation: frs.lpdjs.fr/guide/hono


Servers (admin UI · CRUD REST · Firestore → SQL sync)

A single unified factory binds all servers to your repository registry. Per-repo repo: … is no longer needed — the registry key drives both the runtime binding and the inferred model type for fieldsConfig autocomplete.

import { createServers } from "@lpdjs/firestore-repo-service";
import { onRequest } from "firebase-functions/v2/https";
import { BigQueryAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery";
import { BigQuery } from "@google-cloud/bigquery";
import { PubSub } from "@google-cloud/pubsub";
import * as firestoreTriggers from "firebase-functions/v2/firestore";
import * as pubsubHandler from "firebase-functions/v2/pubsub";

const servers = createServers(repos, {
  onRequest,
  httpsOptions: { invoker: "public" },
});

// Admin UI — repo auto-injected from the key, fieldsConfig typed against the model
export const admin = servers.admin({
  basePath: "/admin",
  auth: { type: "basic", username: "admin", password: "secret" },
  repos: {
    posts: {
      path: "posts",
      fieldsConfig: { title: ["create", "mutable"], status: ["filterable"] },
      allowDelete: true,
    },
    users: { path: "users" },
  },
});

// CRUD REST API
export const api = servers.crud({
  basePath: "/api",
  repos: {
    posts: { path: "posts", allowDelete: true },
    users: { path: "users" },
  },
  openapi: { title: "My API", version: "1.0.0" },
});

// Firestore → BigQuery sync (triggers + worker + admin Cloud Functions)
export const { functions } = servers.sync({
  deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
  adapter: new BigQueryAdapter({
    bigquery: new BigQuery({ projectId: "my-project" }),
    projectId: "my-project",
    datasetId: "firestore_sync",
    maxStaleness: "INTERVAL 15 MINUTE",
  }),
  topicPrefix: "firestore-sync",
  autoMigrate: true,
  admin: {
    auth: { type: "basic", username: "admin", password: "secret" },
    featuresFlag: { healthCheck: true, manualSync: true, configCheck: true },
  },
  repos: {
    users: { tableName: "users", columnMap: { docId: "user_id" } },
    posts: { columnMap: { docId: "post_id" } },
    comments: { triggerPath: "posts/{postId}/comments/{docId}" },
  },
});

// Spread Cloud Functions into your exports
export const {
  users_onCreate,
  users_onUpdate,
  users_onDelete,
  sync_users,
  posts_onCreate,
  posts_onUpdate,
  posts_onDelete,
  sync_posts,
  comments_onCreate,
  comments_onUpdate,
  comments_onDelete,
  sync_comments,
  adminsync,
} = functions;

When onRequest is passed to createServers, servers.admin() and servers.crud() return ready-to-export Cloud Functions. Without it, they return raw HTTP handlers you can wrap yourself.

The sync admin endpoint (/) exposes a UI for health checks, force-sync, queue inspection, and GCP config verification.

For a custom SQL backend, implement the SqlAdapter interface:

import type { SqlAdapter } from "@lpdjs/firestore-repo-service/sync";

class MyAdapter implements SqlAdapter {
  // tableExists, getTableColumns, createTable, upsertRows, deleteRows, executeRaw
}

Full sync documentation: frs.lpdjs.fr/guide/sync


Testing

# Run emulator + tests (watch mode)
bun run test:watch

# Two-terminal alternative
bun run emulator  # terminal 1
bun run test      # terminal 2

Firestore emulator runs on localhost:8080, UI on http://localhost:4000.

License

MIT

Installation

npm install @lpdjs/firestore-repo-service firebase-admin

Quick start

Define your models

interface UserModel {
  docId: string;
  email: string;
  name: string;
  age: number;
  isActive: boolean;
}

interface PostModel {
  docId: string;
  userId: string;
  title: string;
  status: "draft" | "published";
}

Create the repository mapping

import {
  createRepositoryConfig,
  buildRepositoryRelations,
  createRepositoryMapping,
} from "@lpdjs/firestore-repo-service";
import { doc } from "firebase/firestore";
import type { Firestore } from "firebase/firestore";

const repositoryMapping = {
  users: createRepositoryConfig<UserModel>()({
    path: "users",
    isGroup: false,
    foreignKeys: ["docId", "email"] as const,
    queryKeys: ["name", "isActive"] as const,
    refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
  }),
  posts: createRepositoryConfig<PostModel>()({
    path: "posts",
    isGroup: false,
    foreignKeys: ["docId", "userId"] as const,
    queryKeys: ["status"] as const,
    refCb: (db: Firestore, docId: string) => doc(db, "posts", docId),
  }),
};

// Optional: add relations
const mappingWithRelations = buildRepositoryRelations(repositoryMapping, {
  posts: {
    userId: { repo: "users", key: "docId", type: "one" as const },
  },
});

export const repos = createRepositoryMapping(() => db, mappingWithRelations);

Use the repositories

// Fetch a single document
const user = await repos.users.get.byDocId("user123");
const userByEmail = await repos.users.get.byEmail("[email protected]");

// Query multiple documents
const activeUsers = await repos.users.query.byIsActive(true);

// With query options
const results = await repos.users.query.byIsActive(true, {
  where: [["age", ">=", 18]],
  orderBy: [{ field: "name", direction: "asc" }],
  limit: 50,
});

// Populate a relation
const post = await repos.posts.get.byDocId("post123");
if (post) {
  const postWithUser = await repos.posts.populate(post, "userId");
  console.log(postWithUser.populated.users?.name); // type-safe
}

// Update
const updated = await repos.users.update("user123", {
  name: "New name",
  age: 31,
});

API reference

createRepositoryConfig()

| Option | Description | | ------------- | --------------------------------------------------- | | path | Collection path in Firestore | | isGroup | true for collection group, false for simple | | foreignKeys | Keys for get.by* methods (single document lookup) | | queryKeys | Keys for query.by* methods (multi-document query) | | refCb | Function that returns the document reference |

Sub-collection example:

comments: createRepositoryConfig<CommentModel>()({
  path: "comments",
  isGroup: true,
  foreignKeys: ["docId"] as const,
  queryKeys: ["postId", "userId"] as const,
  refCb: (db, postId, commentId) =>
    doc(db, "posts", postId, "comments", commentId),
});

Query options

interface QueryOptions<T> {
  where?: [keyof T, WhereFilterOp, any][]; // AND conditions
  orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
  orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
  limit?: number;
  offset?: number;
  select?: (keyof T)[];
  startAt?: DocumentSnapshot | any[];
  startAfter?: DocumentSnapshot | any[];
  endAt?: DocumentSnapshot | any[];
  endBefore?: DocumentSnapshot | any[];
}

CRUD

// Create (auto-generated ID)
const newUser = await repos.users.create({ email: "...", name: "...", age: 25, isActive: true });

// Set (create or replace)
await repos.users.set("user123", { ... });

// Set with merge
await repos.users.set("user123", { age: 31 }, { merge: true });

// Update (partial)
await repos.users.update("user123", { age: 32 });

// Delete
await repos.users.delete("user123");

// Document ref
const ref = repos.users.documentRef("user123");

// Raw collection ref
const colRef = repos.users.ref;

Batch & Bulk

// Atomic batch (max 500 operations)
const batch = repos.users.batch.create();
batch.set(repos.users.documentRef("u1"), { ... });
batch.update(repos.users.documentRef("u2"), { age: 25 });
batch.delete(repos.users.documentRef("u3"));
await batch.commit();

// Bulk (auto-split into batches of 500)
await repos.users.bulk.set([
  { docRef: repos.users.documentRef("u1"), data: { ... }, merge: true },
]);
await repos.users.bulk.update([...]);
await repos.users.bulk.delete([...]);

Real-time listeners

const unsubscribe = repos.users.query.onSnapshot(
  { where: [["isActive", "==", true]] },
  (users) => console.log(users),
);
unsubscribe();

Pagination

const firstPage = await repos.users.query.by({
  orderBy: [{ field: "createdAt", direction: "desc" }],
  limit: 10,
});

const nextPage = await repos.users.query.by({
  orderBy: [{ field: "createdAt", direction: "desc" }],
  startAfter: firstPage[firstPage.length - 1],
  limit: 10,
});

// Paginate with relations
const page = await repos.posts.query.paginate({
  pageSize: 10,
  include: [{ relation: "userId", select: ["docId", "name", "email"] }],
});

Aggregations

import { count, sum, average } from "@lpdjs/firestore-repo-service";

const activeCount = await repos.users.aggregate.count({
  where: [["isActive", "==", true]],
});
const totalViews = await repos.posts.aggregate.sum("views");
const avgAge = await repos.users.aggregate.average("age");

Transactions

const result = await repos.users.transaction.run(async (txn) => {
  const user = await txn.get(repos.users.documentRef("user123"));
  if (user.exists()) {
    txn.update(repos.users.documentRef("user123"), {
      age: user.data().age + 1,
    });
  }
  return { success: true };
});

OR queries

// (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
const users = await repos.users.query.by({
  orWhere: [
    [
      ["status", "==", "active"],
      ["age", ">=", 18],
    ],
    [
      ["status", "==", "pending"],
      ["verified", "==", true],
    ],
  ],
});

Servers (admin UI · CRUD REST · Firestore → SQL sync)

A single unified factory binds all servers to your repository registry. Per-repo repo: … is no longer needed — the registry key drives both the runtime binding and the inferred model type for fieldsConfig autocomplete.

import { createServers } from "@lpdjs/firestore-repo-service";
import { onRequest } from "firebase-functions/v2/https";
import { BigQueryAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery";
import { BigQuery } from "@google-cloud/bigquery";
import { PubSub } from "@google-cloud/pubsub";
import * as firestoreTriggers from "firebase-functions/v2/firestore";
import * as pubsubHandler from "firebase-functions/v2/pubsub";

const servers = createServers(repos, {
  onRequest,
  httpsOptions: { invoker: "public" },
});

// Admin UI — repo auto-injected from the key, fieldsConfig typed against the model
export const admin = servers.admin({
  basePath: "/admin",
  auth: { type: "basic", username: "admin", password: "secret" },
  repos: {
    posts: {
      path: "posts",
      fieldsConfig: { title: ["create", "mutable"], status: ["filterable"] },
      allowDelete: true,
    },
    users: { path: "users" },
  },
});

// CRUD REST API
export const api = servers.crud({
  basePath: "/api",
  repos: {
    posts: { path: "posts", allowDelete: true },
    users: { path: "users" },
  },
  openapi: { title: "My API", version: "1.0.0" },
});

// Firestore → BigQuery sync (triggers + worker + admin Cloud Functions)
export const { functions } = servers.sync({
  deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
  adapter: new BigQueryAdapter({
    bigquery: new BigQuery({ projectId: "my-project" }),
    projectId: "my-project",
    datasetId: "firestore_sync",
    maxStaleness: "INTERVAL 15 MINUTE",
  }),
  topicPrefix: "firestore-sync",
  autoMigrate: true,
  admin: {
    auth: { type: "basic", username: "admin", password: "secret" },
    featuresFlag: { healthCheck: true, manualSync: true, configCheck: true },
  },
  repos: {
    users: { tableName: "users", columnMap: { docId: "user_id" } },
    posts: { columnMap: { docId: "post_id" } },
    comments: { triggerPath: "posts/{postId}/comments/{docId}" },
  },
});

// Spread Cloud Functions into your exports
export const {
  users_onCreate,
  users_onUpdate,
  users_onDelete,
  sync_users,
  posts_onCreate,
  posts_onUpdate,
  posts_onDelete,
  sync_posts,
  comments_onCreate,
  comments_onUpdate,
  comments_onDelete,
  sync_comments,
  adminsync,
} = functions;

When onRequest is passed to createServers, servers.admin() and servers.crud() return ready-to-export Cloud Functions. Without it, they return raw HTTP handlers you can wrap yourself.

The sync admin endpoint (/) exposes a UI for health checks, force-sync, queue inspection, and GCP config verification.

For a custom SQL backend, implement the SqlAdapter interface:

import type { SqlAdapter } from "@lpdjs/firestore-repo-service/sync";

class MyAdapter implements SqlAdapter {
  // tableExists, getTableColumns, createTable, upsertRows, deleteRows, executeRaw
}

Full sync documentation: frs.lpdjs.fr/guide/sync

Testing

# Run emulator + tests (watch mode)
bun run test:watch

# Two-terminal alternative
bun run emulator  # terminal 1
bun run test      # terminal 2

Firestore emulator runs on localhost:8080, UI on http://localhost:4000.

License

MIT