소스 정보
- 저장소
- alinaqi/maggy
- 최근 소스 활동
- 2026년 4월 7일 05:21
- 감지된 SKILL.md 언어
- 영어
- 스타
- 705
- 포크
- 56
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/alinaqi/maggy --skill firebase명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | firebase |
| description | Firebase Firestore, Auth, Storage, real-time listeners, security rules |
| when-to-use | When working with Firebase services |
| user-invocable | false |
| paths | ["**/firebase*","firestore.rules","storage.rules","firebase.json"] |
| effort | medium |
Firebase/Firestore patterns for web and mobile applications with real-time data, offline support, and security rules.
Sources: Firebase Docs | Firestore Best Practices | Security Rules
Denormalize with purpose, secure with rules, scale horizontally.
Firestore is a document database - embrace denormalization for read efficiency. Security rules are your server-side validation. Design for your access patterns.
| Service | Purpose |
|---|---|
| Firestore | NoSQL document database with real-time sync |
| Authentication | User auth, OAuth, anonymous sessions |
| Storage | File uploads with security rules |
| Functions | Serverless backend (Node.js) |
| Hosting | Static site + CDN |
| Extensions | Pre-built solutions (Stripe, Algolia, etc.) |
# Install globally
npm install -g firebase-tools
# Login
firebase login
# Initialize in project
firebase init
firebase init emulators
# Start local development
firebase emulators:start
project/
├── firebase.json # Firebase config
├── firestore.rules # Security rules
├── firestore.indexes.json # Composite indexes
├── storage.rules # Storage security rules
└── functions/ # Cloud Functions
├── src/
├── package.json
└── tsconfig.json
// Good: Flat documents with all needed data
interface Post {
id: string;
title: string;
content: string;
authorId: string;
authorName: string; // Denormalized for display
authorAvatar: string; // Denormalized
tags: string[];
likeCount: number; // Aggregated counter
createdAt: Timestamp;
updatedAt: Timestamp;
}
// Collection: posts/{postId}
// Use subcollections for:
// 1. Unbounded lists (comments, messages)
// 2. Data with different access patterns
// 3. Data that grows independently
// posts/{postId}/comments/{commentId}
interface Comment {
id: string;
text: string;
authorId: string;
authorName: string;
createdAt: Timestamp;
}
// Pattern 1: Embedded data (bounded, always needed)
interface User {
id: string;
email: string;
profile: {
displayName: string;
bio: string;
avatar: string;
};
settings: {
notifications: boolean;
theme: 'light' | 'dark';
};
}
// Pattern 2: Reference with denormalization
interface Order {
id: string;
userId: string;
userEmail: string; // Denormalized for display
items: OrderItem[]; // Embedded (bounded)
total: number;
status: 'pending' | 'paid' | 'shipped';
}
// Pattern 3: Aggregation documents
// Keep counters in parent document
interface Channel {
id: string;
name: string;
memberCount: number; // Updated via Cloud Function
messageCount: number;
}
// lib/firebase.ts
import { initializeApp, getApps } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { getStorage, connectStorageEmulator } from 'firebase/storage';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID
};
// Initialize only once
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export const db = getFirestore(app);
export const auth = getAuth(app);
export storage = (app);
(process.. === ) {
(db, , );
(auth, );
(storage, , );
}
import {
collection,
doc,
getDoc,
getDocs,
addDoc,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
startAfter,
serverTimestamp,
Timestamp
} from 'firebase/firestore';
import { db } from './firebase';
// Create
async function createPost(data: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>) {
const docRef = await addDoc(collection(db, 'posts'), {
...data,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
});
return docRef.id;
}
// Read single document
async function getPost(postId: string): Promise<Post | null> {
const docSnap = await getDoc(doc(db, 'posts', postId));
if (!docSnap.exists()) return null;
return { id: docSnap., ...docSnap.() } ;
}
() {
q = (
(db, ),
(, , authorId),
(, ),
(pageSize)
);
snapshot = (q);
snapshot..( ({ : doc., ...doc.() } ));
}
() {
q = (
(db, ),
(, ),
(lastDoc.),
(pageSize)
);
snapshot = (q);
snapshot..( ({ : doc., ...doc.() } ));
}
() {
((db, , postId), {
...data,
: ()
});
}
() {
((db, , postId));
}
import { onSnapshot, QuerySnapshot, DocumentSnapshot } from 'firebase/firestore';
// Listen to single document
function subscribeToPost(
postId: string,
onData: (post: Post | null) => void,
onError: (error: Error) => void
) {
return onSnapshot(
doc(db, 'posts', postId),
(snapshot: DocumentSnapshot) => {
if (!snapshot.exists()) {
onData(null);
return;
}
onData({ id: snapshot.id, ...snapshot.data() } as Post);
},
onError
);
}
// Listen to collection with query
function subscribeToPosts(
authorId: string,
onData: (posts: Post[]) => void,
onError: (error: Error) => void
) {
const q = query(
collection(db, 'posts'),
(, , authorId),
(, )
);
(
q,
{
posts = snapshot..( ({
: doc.,
...doc.()
} ));
(posts);
},
onError
);
}
() {
[post, setPost] = useState< | >();
[loading, setLoading] = ();
[error, setError] = useState< | >();
( {
unsubscribe = (
postId,
{
(data);
();
},
{
(err);
();
}
);
unsubscribe;
}, [postId]);
{ post, loading, error };
}
import { enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
// Enable offline persistence (call once at startup)
async function enableOffline() {
try {
// Single tab
await enableIndexedDbPersistence(db);
// OR multi-tab (recommended)
await enableMultiTabIndexedDbPersistence(db);
} catch (err: any) {
if (err.code === 'failed-precondition') {
// Multiple tabs open, only works in one
console.warn('Persistence only available in one tab');
} else if (err.code === 'unimplemented') {
// Browser doesn't support
console.warn('Persistence not supported');
}
}
}
// Check if data is from cache
onSnapshot(docRef, (snapshot) => {
const source = snapshot.metadata.fromCache ? 'cache' : 'server';
console.log(`Data from ${source}`);
if (snapshot..) {
.();
}
});
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isAdmin() {
return request.auth.token.admin == true;
}
// Posts collection
match /posts/{postId} {
// Anyone can read published posts
allow read: if resource.data.status == 'published';
// Only authenticated users can create
allow create: if isAuthenticated()
&& request.resource.data.authorId == request.auth.uid
&& request.resource.data.keys().hasAll(['title', 'content', 'authorId']);
// Only author can update
allow update: (resource..)
&& request... == resource..;
allow : (resource..) || ();
match /comments/{commentId} {
allow : ;
allow : ();
allow update, : (resource..);
}
}
match /users/{userId} {
allow : ;
allow : () && (userId);
allow : (userId);
allow : ;
}
match /users/{userId}/private/{=**} {
allow read, : (userId);
}
}
}
match /posts/{postId} {
function isValidPost() {
let data = request.resource.data;
return data.title is string
&& data.title.size() >= 3
&& data.title.size() <= 100
&& data.content is string
&& data.content.size() <= 50000
&& data.tags is list
&& data.tags.size() <= 5;
}
allow create: if isAuthenticated() && isValidPost();
allow update: if isOwner(resource.data.authorId) && isValidPost();
}
# Install emulators
firebase emulators:start
# Run rules tests
npm test
// tests/firestore.rules.test.ts
import { assertFails, assertSucceeds, initializeTestEnvironment } from '@firebase/rules-unit-testing';
describe('Firestore Rules', () => {
let testEnv: RulesTestEnvironment;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: 'test-project',
firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }
});
});
test('unauthenticated users cannot write', async () => {
const unauthedDb = testEnv.unauthenticatedContext().firestore();
await assertFails(
setDoc(doc(unauthedDb, 'posts/test'), { title: 'Test' })
);
});
test('users can only update own posts', async () => {
const aliceDb = testEnv.authenticatedContext('alice').firestore();
const bobDb = testEnv.authenticatedContext('bob').firestore();
// Create as Alice
(
((aliceDb, ), { : , : })
);
(
((bobDb, ), { : })
);
});
});
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
User
} from 'firebase/auth';
import { auth } from './firebase';
// Sign up
async function signUp(email: string, password: string) {
const credential = await createUserWithEmailAndPassword(auth, email, password);
return credential.user;
}
// Sign in
async function signIn(email: string, password: string) {
const credential = await signInWithEmailAndPassword(auth, email, password);
return credential.user;
}
// Sign out
async function logout() {
await signOut(auth);
}
// Auth state listener
function onAuthChange(callback: (user: User | null) => void) {
return onAuthStateChanged(auth, callback);
}
import {
GoogleAuthProvider,
signInWithPopup,
signInWithRedirect
} from 'firebase/auth';
const googleProvider = new GoogleAuthProvider();
async function signInWithGoogle() {
try {
const result = await signInWithPopup(auth, googleProvider);
return result.user;
} catch (error) {
// Handle errors
throw error;
}
}
// functions/src/index.ts
import { onRequest } from 'firebase-functions/v2/https';
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
initializeApp();
const db = getFirestore();
// HTTP endpoint
export const helloWorld = onRequest((request, response) => {
response.json({ message: 'Hello from Firebase!' });
});
// Firestore trigger
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
const snapshot = event.data;
if (!snapshot) return;
const post = snapshot.data();
// Update author's post count
await db.doc(`users/${post.authorId}`).update({
postCount: FieldValue.increment(1)
});
});
// Backend
import { onCall, HttpsError } from 'firebase-functions/v2/https';
export const createPost = onCall(async (request) => {
// Auth check
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Must be logged in');
}
const { title, content } = request.data;
// Validation
if (!title || title.length < 3) {
throw new HttpsError('invalid-argument', 'Title must be at least 3 characters');
}
// Create post
const postRef = await db.collection('posts').add({
title,
content,
authorId: request.auth.uid,
createdAt: FieldValue.serverTimestamp()
});
return { postId: postRef.id };
});
// Frontend
import { getFunctions, httpsCallable } from 'firebase/functions';
const functions = getFunctions();
createPostFn = (functions, );
() {
result = ({ title, content });
result. { : };
}
import { writeBatch, doc } from 'firebase/firestore';
async function batchUpdate(updates: { id: string; data: any }[]) {
const batch = writeBatch(db);
updates.forEach(({ id, data }) => {
batch.update(doc(db, 'posts', id), data);
});
await batch.commit(); // Atomic
}
import { runTransaction, doc, increment } from 'firebase/firestore';
async function likePost(postId: string, userId: string) {
await runTransaction(db, async (transaction) => {
const postRef = doc(db, 'posts', postId);
const likeRef = doc(db, 'posts', postId, 'likes', userId);
const postSnap = await transaction.get(postRef);
if (!postSnap.exists()) throw new Error('Post not found');
const likeSnap = await transaction.get(likeRef);
if (likeSnap.exists()) throw new Error('Already liked');
transaction.set(likeRef, { createdAt: serverTimestamp() });
transaction.update(postRef, { likeCount: increment(1) });
});
}
// firestore.indexes.json
{
"indexes": [
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "authorId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "tags", "arrayConfig"
# Deploy indexes
firebase deploy --only firestore:indexes
# Project setup
firebase login # Authenticate
firebase init # Initialize project
firebase projects:list # List projects
# Emulators
firebase emulators:start # Start all emulators
firebase emulators:start --only firestore,auth # Specific emulators
# Deploy
firebase deploy # Deploy everything
firebase deploy --only firestore # Deploy rules + indexes
firebase deploy --only functions # Deploy functions
firebase deploy --only hosting # Deploy hosting
# Functions
cd functions && npm run build # Build TypeScript
firebase functions:log # View logs