better-auth-firestore 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import { type AppOptions } from "firebase-admin/app";
2
+ import { type Firestore } from "firebase-admin/firestore";
3
+ /**
4
+ * Initialize or reuse a Firestore instance safely (useful in serverless).
5
+ * If an app with the provided name exists, reuse it; otherwise initialize.
6
+ */
7
+ export declare function initFirestore(options?: AppOptions & {
8
+ name?: string;
9
+ }): Firestore;
@@ -0,0 +1,16 @@
1
+ import { getApps, initializeApp } from "firebase-admin/app";
2
+ import { getFirestore, initializeFirestore, } from "firebase-admin/firestore";
3
+ /**
4
+ * Initialize or reuse a Firestore instance safely (useful in serverless).
5
+ * If an app with the provided name exists, reuse it; otherwise initialize.
6
+ */
7
+ export function initFirestore(options = {}) {
8
+ // Reuse existing app by name if exists, otherwise initialize
9
+ const apps = getApps();
10
+ const app = options.name
11
+ ? apps.find((a) => a.name === options.name)
12
+ : apps[0];
13
+ if (app)
14
+ return getFirestore(app);
15
+ return initializeFirestore(initializeApp(options, options.name));
16
+ }
@@ -0,0 +1,28 @@
1
+ import type { Firestore } from "firebase-admin/firestore";
2
+ import type { NamingStrategy } from "./types";
3
+ export interface FirestoreIndexField {
4
+ fieldPath: string;
5
+ order: "ASCENDING" | "DESCENDING";
6
+ }
7
+ export interface FirestoreIndex {
8
+ collectionGroup: string;
9
+ queryScope: "COLLECTION" | "COLLECTION_GROUP";
10
+ fields: FirestoreIndexField[];
11
+ }
12
+ /**
13
+ * Returns all required Firestore index definitions for Better Auth adapter.
14
+ * Supports both default and snake_case naming strategies.
15
+ */
16
+ export declare function getRequiredIndexes(namingStrategy?: NamingStrategy): FirestoreIndex[];
17
+ /**
18
+ * Creates Firestore indexes programmatically using the Firestore REST API.
19
+ * Note: Firebase Admin SDK doesn't have direct index creation methods,
20
+ * so this function provides a helper to format indexes for manual creation
21
+ * or uses the REST API if credentials are available.
22
+ *
23
+ * @param firestore - Firestore instance
24
+ * @param projectId - Firebase project ID
25
+ * @param namingStrategy - Naming strategy to use ("default" or "snake_case")
26
+ * @returns Array of index definitions that can be used to create indexes
27
+ */
28
+ export declare function createFirestoreIndexes(firestore: Firestore, projectId: string, namingStrategy?: NamingStrategy): Promise<FirestoreIndex[]>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Returns all required Firestore index definitions for Better Auth adapter.
3
+ * Supports both default and snake_case naming strategies.
4
+ */
5
+ export function getRequiredIndexes(namingStrategy = "default") {
6
+ const isSnakeCase = namingStrategy === "snake_case";
7
+ const collectionName = isSnakeCase
8
+ ? "verification_tokens"
9
+ : "verificationTokens";
10
+ const createdAtField = isSnakeCase ? "created_at" : "createdAt";
11
+ return [
12
+ {
13
+ collectionGroup: collectionName,
14
+ queryScope: "COLLECTION",
15
+ fields: [
16
+ {
17
+ fieldPath: "identifier",
18
+ order: "ASCENDING",
19
+ },
20
+ {
21
+ fieldPath: createdAtField,
22
+ order: "DESCENDING",
23
+ },
24
+ ],
25
+ },
26
+ ];
27
+ }
28
+ /**
29
+ * Creates Firestore indexes programmatically using the Firestore REST API.
30
+ * Note: Firebase Admin SDK doesn't have direct index creation methods,
31
+ * so this function provides a helper to format indexes for manual creation
32
+ * or uses the REST API if credentials are available.
33
+ *
34
+ * @param firestore - Firestore instance
35
+ * @param projectId - Firebase project ID
36
+ * @param namingStrategy - Naming strategy to use ("default" or "snake_case")
37
+ * @returns Array of index definitions that can be used to create indexes
38
+ */
39
+ export async function createFirestoreIndexes(firestore, projectId, namingStrategy = "default") {
40
+ const indexes = getRequiredIndexes(namingStrategy);
41
+ // Note: The Firebase Admin SDK doesn't provide direct methods to create indexes.
42
+ // Indexes must be created via:
43
+ // 1. Firebase Console (Firestore provides URLs in error messages)
44
+ // 2. Firebase CLI: `firebase deploy --only firestore:indexes`
45
+ // 3. Firestore REST API (requires additional authentication)
46
+ // This function returns the index definitions for reference.
47
+ // Users should deploy indexes using one of the methods above.
48
+ console.log(`[Firestore Indexes] ${indexes.length} index definition(s) generated for project: ${projectId}`);
49
+ console.log(`[Firestore Indexes] Deploy indexes using: firebase deploy --only firestore:indexes`);
50
+ return indexes;
51
+ }
@@ -0,0 +1,3 @@
1
+ export { firestoreAdapter } from "./firebase-adapter";
2
+ export { initFirestore } from "./firestore";
3
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { firestoreAdapter } from "./firebase-adapter";
2
+ export { initFirestore } from "./firestore";
3
+ export * from "./types";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Generate a Firebase Console URL for creating the required Firestore composite index.
3
+ * This URL will pre-fill the index creation form with the correct configuration,
4
+ * similar to the URLs provided in Firestore error messages.
5
+ *
6
+ * @param projectId - Your Firebase project ID
7
+ * @param databaseId - Your Firestore database ID (defaults to "(default)")
8
+ * @param collectionName - The collection name (defaults to "verification")
9
+ * @returns A Firebase Console URL with pre-filled index configuration
10
+ */
11
+ export declare function generateIndexSetupUrl(projectId: string, databaseId?: string, collectionName?: string): string;
12
+ /**
13
+ * Get the index configuration object for the verification collection.
14
+ * This can be used to create firestore.indexes.json file.
15
+ */
16
+ export declare function getIndexConfig(collectionName?: string): {
17
+ indexes: {
18
+ collectionGroup: string;
19
+ queryScope: string;
20
+ fields: {
21
+ fieldPath: string;
22
+ order: string;
23
+ }[];
24
+ }[];
25
+ fieldOverrides: never[];
26
+ };
package/dist/setup.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Generate a Firebase Console URL for creating the required Firestore composite index.
3
+ * This URL will pre-fill the index creation form with the correct configuration,
4
+ * similar to the URLs provided in Firestore error messages.
5
+ *
6
+ * @param projectId - Your Firebase project ID
7
+ * @param databaseId - Your Firestore database ID (defaults to "(default)")
8
+ * @param collectionName - The collection name (defaults to "verification")
9
+ * @returns A Firebase Console URL with pre-filled index configuration
10
+ */
11
+ export function generateIndexSetupUrl(projectId, databaseId = "(default)", collectionName = "verification") {
12
+ // For protobuf, use the original format (keep (default) as-is)
13
+ const protobufDatabaseId = databaseId;
14
+ // Construct the index resource path for protobuf
15
+ // Format: projects/{projectId}/databases/{databaseId}/collectionGroups/{collectionName}/indexes/
16
+ const indexPath = `projects/${projectId}/databases/${protobufDatabaseId}/collectionGroups/${collectionName}/indexes/`;
17
+ // Build protobuf-encoded message
18
+ // Based on Firebase's actual error message format:
19
+ // Field 1 (tag 0x0a, wire type 2): parent path (string)
20
+ const pathTag = Buffer.from([0x0a]); // Field 1, wire type 2
21
+ const pathLength = Buffer.from([indexPath.length]);
22
+ const pathData = Buffer.from(indexPath, "utf8");
23
+ // Field 2 (tag 0x10, wire type 0): query scope (1 = COLLECTION)
24
+ const scopeTag = Buffer.from([0x10, 0x01]); // Field 2, value 1
25
+ // Field 3 (tag 0x1a, wire type 2): fields array - identifier
26
+ const identifierField = Buffer.concat([
27
+ Buffer.from([0x1a, 0x0e]), // Field 3, length 14
28
+ Buffer.from([0x0a, 0x0a]), // Nested field 1, length 10
29
+ Buffer.from("identifier", "utf8"),
30
+ Buffer.from([0x10, 0x01]), // Nested field 2, value 1 (ASCENDING)
31
+ ]);
32
+ // Field 4 (tag 0x1a, wire type 2): fields array - createdAt
33
+ const createdAtField = Buffer.concat([
34
+ Buffer.from([0x1a, 0x0d]), // Field 3, length 13
35
+ Buffer.from([0x0a, 0x09]), // Nested field 1, length 9
36
+ Buffer.from("createdAt", "utf8"),
37
+ Buffer.from([0x10, 0x02]), // Nested field 2, value 2 (DESCENDING)
38
+ ]);
39
+ // Field 5 (tag 0x1a, wire type 2): fields array - __name__
40
+ const nameField = Buffer.concat([
41
+ Buffer.from([0x1a, 0x0c]), // Field 3, length 12
42
+ Buffer.from([0x0a, 0x08]), // Nested field 1, length 8
43
+ Buffer.from("__name__", "utf8"),
44
+ Buffer.from([0x10, 0x02]), // Nested field 2, value 2 (DESCENDING)
45
+ ]);
46
+ // Combine all parts
47
+ const protobuf = Buffer.concat([
48
+ pathTag,
49
+ pathLength,
50
+ pathData,
51
+ scopeTag,
52
+ identifierField,
53
+ createdAtField,
54
+ nameField,
55
+ ]);
56
+ const createComposite = protobuf.toString("base64");
57
+ // For default database, omit the database path entirely
58
+ if (databaseId === "(default)") {
59
+ return `https://console.firebase.google.com/project/${projectId}/firestore/indexes?create_composite=${createComposite}`;
60
+ }
61
+ // For custom databases, include the database path (convert (default) to -default- format)
62
+ const urlDatabaseId = databaseId === "(default)" ? "-default-" : databaseId;
63
+ return `https://console.firebase.google.com/project/${projectId}/firestore/databases/${urlDatabaseId}/indexes?create_composite=${createComposite}`;
64
+ }
65
+ /**
66
+ * Get the index configuration object for the verification collection.
67
+ * This can be used to create firestore.indexes.json file.
68
+ */
69
+ export function getIndexConfig(collectionName = "verification") {
70
+ return {
71
+ indexes: [
72
+ {
73
+ collectionGroup: collectionName,
74
+ queryScope: "COLLECTION",
75
+ fields: [
76
+ {
77
+ fieldPath: "identifier",
78
+ order: "ASCENDING",
79
+ },
80
+ {
81
+ fieldPath: "createdAt",
82
+ order: "DESCENDING",
83
+ },
84
+ {
85
+ fieldPath: "__name__",
86
+ order: "DESCENDING",
87
+ },
88
+ ],
89
+ },
90
+ ],
91
+ fieldOverrides: [],
92
+ };
93
+ }
@@ -0,0 +1,22 @@
1
+ import type { DBAdapterDebugLogOption } from "better-auth/adapters";
2
+ import type { AppOptions } from "firebase-admin/app";
3
+ import type { Firestore } from "firebase-admin/firestore";
4
+ export type NamingStrategy = "snake_case" | "default";
5
+ export interface FirestoreCollectionsOverride {
6
+ users?: string;
7
+ sessions?: string;
8
+ accounts?: string;
9
+ verificationTokens?: string;
10
+ }
11
+ export interface FirestoreAdapterConfig extends AppOptions {
12
+ name?: string;
13
+ firestore?: Firestore;
14
+ namingStrategy?: NamingStrategy;
15
+ collections?: FirestoreCollectionsOverride;
16
+ debugLogs?: DBAdapterDebugLogOption;
17
+ }
18
+ export interface InternalNormalizedConfig {
19
+ firestore: Firestore;
20
+ preferSnakeCase: boolean;
21
+ collections: Required<Required<FirestoreAdapterConfig>["collections"]>;
22
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { Firestore } from "firebase-admin/firestore";
2
+ export type FieldMapper = {
3
+ toDb: (field: string) => string;
4
+ fromDb: (field: string) => string;
5
+ };
6
+ export declare function mapFieldsFactory(preferSnakeCase?: boolean): FieldMapper;
7
+ export declare function getConverter<Document extends Record<string, any>>(options: {
8
+ excludeId?: boolean;
9
+ preferSnakeCase?: boolean;
10
+ }): {
11
+ toFirestore(object: Document): Record<string, unknown>;
12
+ fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>): Document;
13
+ };
14
+ export declare function getOneDoc<T>(querySnapshot: FirebaseFirestore.Query<T>): Promise<T | null>;
15
+ export declare function getDoc<T>(docRef: FirebaseFirestore.DocumentReference<T>): Promise<T | null>;
16
+ export declare function deleteDocs<T>(querySnapshot: FirebaseFirestore.Query<T>): Promise<void>;
17
+ export declare function collectionsFactory(db: Firestore, preferSnakeCase: boolean | undefined, collections: {
18
+ users: string;
19
+ sessions: string;
20
+ accounts: string;
21
+ verificationTokens: string;
22
+ }): {
23
+ users: FirebaseFirestore.CollectionReference<any, FirebaseFirestore.DocumentData>;
24
+ sessions: FirebaseFirestore.CollectionReference<any, FirebaseFirestore.DocumentData>;
25
+ accounts: FirebaseFirestore.CollectionReference<any, FirebaseFirestore.DocumentData>;
26
+ verification_tokens: FirebaseFirestore.CollectionReference<any, FirebaseFirestore.DocumentData>;
27
+ };
package/dist/utils.js ADDED
@@ -0,0 +1,78 @@
1
+ import { Timestamp } from "firebase-admin/firestore";
2
+ const MAP_TO_FIRESTORE = {
3
+ userId: "user_id",
4
+ sessionToken: "session_token",
5
+ providerAccountId: "provider_account_id",
6
+ emailVerified: "email_verified",
7
+ };
8
+ const MAP_FROM_FIRESTORE = Object.fromEntries(Object.entries(MAP_TO_FIRESTORE).map(([k, v]) => [v, k]));
9
+ const identity = (x) => x;
10
+ export function mapFieldsFactory(preferSnakeCase) {
11
+ if (preferSnakeCase) {
12
+ return {
13
+ toDb: (field) => MAP_TO_FIRESTORE[field] ?? field,
14
+ fromDb: (field) => MAP_FROM_FIRESTORE[field] ?? field,
15
+ };
16
+ }
17
+ return { toDb: identity, fromDb: identity };
18
+ }
19
+ export function getConverter(options) {
20
+ const mapper = mapFieldsFactory(options?.preferSnakeCase);
21
+ return {
22
+ toFirestore(object) {
23
+ const document = {};
24
+ for (const key in object) {
25
+ if (key === "id")
26
+ continue;
27
+ const value = object[key];
28
+ if (value !== undefined) {
29
+ document[mapper.toDb(key)] = value;
30
+ }
31
+ }
32
+ return document;
33
+ },
34
+ fromFirestore(snapshot) {
35
+ const document = snapshot.data();
36
+ const object = {};
37
+ if (!options?.excludeId)
38
+ object.id = snapshot.id;
39
+ for (const key in document) {
40
+ let value = document[key];
41
+ if (value instanceof Timestamp)
42
+ value = value.toDate();
43
+ object[mapper.fromDb(key)] = value;
44
+ }
45
+ return object;
46
+ },
47
+ };
48
+ }
49
+ export async function getOneDoc(querySnapshot) {
50
+ const querySnap = await querySnapshot.limit(1).get();
51
+ return querySnap.docs[0]?.data() ?? null;
52
+ }
53
+ export async function getDoc(docRef) {
54
+ const docSnap = await docRef.get();
55
+ return docSnap.data() ?? null;
56
+ }
57
+ export async function deleteDocs(querySnapshot) {
58
+ const querySnap = await querySnapshot.get();
59
+ for (const doc of querySnap.docs) {
60
+ await doc.ref.delete();
61
+ }
62
+ }
63
+ export function collectionsFactory(db, preferSnakeCase = false, collections) {
64
+ return {
65
+ users: db
66
+ .collection(collections.users)
67
+ .withConverter(getConverter({ preferSnakeCase })),
68
+ sessions: db
69
+ .collection(collections.sessions)
70
+ .withConverter(getConverter({ preferSnakeCase })),
71
+ accounts: db
72
+ .collection(collections.accounts)
73
+ .withConverter(getConverter({ preferSnakeCase })),
74
+ verification_tokens: db
75
+ .collection(collections.verificationTokens)
76
+ .withConverter(getConverter({ preferSnakeCase, excludeId: true })),
77
+ };
78
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "better-auth-firestore",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Firestore adapter for Better Auth (Firebase Admin SDK)",
6
+ "author": "Slava Yultyyev <yultyyev@gmail.com>",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "clean": "rimraf dist",
27
+ "test": "./tests/test.sh",
28
+ "lint": "biome check --formatter-enabled=false",
29
+ "lint:fix": "biome check --write",
30
+ "lint:fix:unsafe": "biome check --write --unsafe",
31
+ "prepublishOnly": "pnpm run build"
32
+ },
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "packageManager": "pnpm@10.0.0",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/yultyyev/better-auth-firestore.git"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "keywords": [
45
+ "better-auth",
46
+ "firebase",
47
+ "firestore",
48
+ "adapter",
49
+ "authentication"
50
+ ],
51
+ "peerDependencies": {
52
+ "better-auth": "*",
53
+ "firebase-admin": ">=13",
54
+ "typescript": "^5.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@biomejs/biome": "^2.3.2",
58
+ "@semantic-release/changelog": "^6.0.3",
59
+ "@semantic-release/commit-analyzer": "^13.0.0",
60
+ "@semantic-release/github": "^11.0.1",
61
+ "@semantic-release/npm": "^12.0.1",
62
+ "@semantic-release/release-notes-generator": "^12.1.0",
63
+ "rimraf": "^6.0.1",
64
+ "semantic-release": "^24.0.0",
65
+ "typescript": "^5.9.3",
66
+ "vitest": "^4.0.5"
67
+ }
68
+ }