Migration Supabase → Firebase : Brancher un Formulaire de Contact sur Firestore Sans Backend
شائع شدہ: 29 April 2026
- سين: ایک فارم جو کچھ بھی محفوظ نہیں کرتا
- Firebase کی جگہ Supabase کیوں؟
- مرحلہ 1 : فائر بیس پروجیکٹ بنائیں
- فاز 2 : Submission جاواسکریپٹ کو دوبارہ لکھیں
- فاز 3 : Supabase کے مورت کوڈ کو صاف کریں
- فاز 4 : فوٹر کو ترتیب دیں
- مرحلہ 5 : خاتمہ شدہ معماری
- یہ ماگریش dogfooding کے بارے میں کیا کہتی ہے
- اگلے مراحل (backlog)
- حوالہ جات
مہینوں تک، اس سائیٹ کا رابطہ فارم موک جاواسکریپت پر چلا — 85٪ کامیابی کی وعدہ، ایک جھوٹا Firestore، صفر ذخیرے شدہ ڈیٹا۔ ابتداء، Supabase 백엔ڈ کے ساتھ Google Apps Script کے ذریعے میل نوٹیفیکیشنز کے لیے plán بنایا گیا تھا۔ Usay chor diya gaya۔ آج، میں Firebase Firestore کی طرف migration کی وضاحت کر رہا ہوں:پروجیکٹ کی بنائیدوری، سکیورٹی کے قواعد، JS کی dubarah likhai، Supabase کی morto code ki safā’i۔ اور اس انتخاب کی وجہ سے یہ بتاتا ہے کہ وہ توسیعِ发展的 falsafa سے متعلق ہے۔
سين: ایک فارم جو کچھ بھی محفوظ نہیں کرتا
یہ سائٹ JBake کے ذریعے بنائی گئی ہے، میرا Gradle پلاگین`bakery`. یہ 100 % Staic ہے — بیک اینڈ یا ڈیٹابیس نہیں۔ مگر میرے پاس ایک رابطہ فورم ہے۔ یہ صفحہ`contact.html`موجود ہے، HTML تیار ہے (필د: نام، ای میل، فون نمبر، موضوع، پیغام، HTML5 کی تصدیق، Spam‑resistant honeypot)، Bootstrap کے اسٹائلز موجود ہیں. بصری طور پر، سب چیز مکمل ہے۔
بصورت تسلیم پر، کچھ نہیں ہوتا۔
const firebaseMock = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.85) {
resolve({ status: 201, message: 'Message stored in Firestore.' });
} else {
reject({ status: 500, message: 'Firestore write failed.' });
}
}, 1500);
});
ایک موک۔ ایک وعدہ جو بنانا کرتا ہے۔ صارف اسپنر دیکھتا ہے، پھر ایک پیغام « پیغام کامیابی سے بھیج دیا گیا ! »۔ لیکن ڈیٹا خلا میں چلتا ہے۔ کسی بھی جگہ کوئی پیغام محفوظ نہیں ہے۔
حالت ایک ٹوٹے ہوئے فرم سے بھی خراب ہے — یہ ایک ٹوٹا ہوا فرم ہے جو جھوٹ بولتا ہے.
Supabase کا ارث
ابتدائی منصوبہ، میں دستاویز کردہ`content/draft/integration_formulaire_contact_supabase.adoc`, امید رکھتا تھا :
-
Supabase کی ایک ڈیٹا بیس جس میں ایک ٹیبل ہے`contacts`اور Row Level Security
-
ایک RPC`handle_contact_form`سروئر طرف
-
ایس کیو ایل ٹریجر گوگل ایپس اسکرپٹ ویب הוק کو پکار رہا ہے
-
Google Apps Script جس کے ذریعے Gmail notification ایمیل بھیجتا ہے
مطابق JavaScript کوڈ ابھی تک موجود ہے`script.js`۔ ایک کلاس ہے`SupabaseManager`Supabase کلائنٹ کو غلوبل متغیرات کے ساتھ شروع کرنے والا`SUPABASE_URL` et SUPABASE_KEY, اور ایک کلاس`ContactFormHandler`جو فارم کا submit واقعہ سنتا ہے اور بولتا`SupabaseManager.submitContactForm()`.
مسئله : یہ عالمی متغیرات بعد از footer میں انجیکت نہیں کیے جاتے۔ یہ`<script src="supabase-js">`اسے ہٹا دیا گیا. کوڈ بلاتا ہے`supabase.createClient()`ایک متغیر پر`supabase`جو موجود نہیں ہے۔ اس لیے:
console.error : 'Supabase client library (supabase-js) is not loaded.'
صرف ڈیٹا محفوظ نہیں کیے جا رہے ہیں، بلکہ submission code بھی مر گیا ہے۔
فنتھم ڈبل سبمیشن
چیزوں کو بدتر کرنے کے لئے، ایک ہےخاموش مسابقتدو ہینڈلرز کے درمیان ایک ہی فارم پر :
-
`contact.js`submit ko suno, mock Firebase ko bulao
-
script.js— زریعے`ContactFormHandler`— Submit کو بھی سن، بلا`SupabaseManager`
دونوں کرتے ہیں`event.preventDefault() + event.stopPropagation()۔ جسے`contact.js`میں سب سے پہلے لوڈ کیا جاتا ہے`footer.thyme, اس کا ہینڈلر پہلے سے جوڑا جاتا ہے۔ یہ پھیلاؤ کو روک دیتا ہے۔`ContactFormHandler`کبھی فعال نہیں ہوگا
یہ nemmeno ایک فعال بگ نہیں ہے — یہ ایک زومبی ہے۔ ایسا کوڈ جو کبھی execute ہونے کا موقع نہیں ملتا۔
@startuml skinparam backgroundColor #FEFEFE title État Initial — Formulaire de Contact rectangle "contact.thyme\n(HTML Bootstrap, honeypot)" as FORM rectangle "footer.thyme\n(Firebase SDK avec config placeholder)" as FOOT rectangle "contact.js\n(mock Firebase, 85% succès)" as CONT rectangle "script.js\n(SupabaseManager + ContactFormHandler, morts)" as SCRIPT rectangle "Utilisateur" as USER USER -> FORM : Soumission du formulaire FORM -> CONT : submit event (attaché en premier) CONT -> CONT : preventDefault + stopPropagation CONT -> CONT : mock Promise (1.5s, aléatoire) note right of CONT ⚠️ Aucune donnée stockée end note FORM ..> SCRIPT : submit event (bloqué par stopPropagation) note right of SCRIPT : ❌ Jamais déclenché note right of SCRIPT : ❌ supabase-js non chargé @enduml
Firebase کی جگہ Supabase کیوں؟
مایگریشن کا فیصلہ اس میں دستاویزی شدہ ہے`AGENT.adoc`(Empty)
Firebase اس کے بعد درج ذیل وجہوں سے منتخب کیا گیا ہے: بہترین مفت پلان، نیٹو فائرestor، انٹیگریٹڈ Cloud Functions، اور گوگل کا زیادہ متناسب ایكو سسٹم۔ موجودہ Supabase امپیLEMENTیشن کو « ⚠️ ترک کردہ » کے ساتھ مارک کیا گیا ہے۔
مفت پلن کے علاوہ، ایک معمارہ وجہ ہے۔ یہ سایت گوگل کے اکوسسٹم میں ہے:ہدف شدہ ریپوزٹری`cheroliv.github.io`, CNAME GitHub Pages پر اشارہ کرتا ہے, Gradle بنڈ GitHub پر JGit کے ذریعے پुश کرتا ہے. گوگل سروس (Firebase) شامل کرنے سے، تیسرے جان سروس (Supabase) کی بجائے، پرکھان کی سطح کم ہوتی ہے.
Firestore نیٹئنٹ مود (Datastore مود نہیں) میں بھی NoSQL دستاویزی منوال سے زیادہ قریب ہے جو میرے ذہن میں ہے: مجموعی، دستاویز، ٹائپڈ فیلڈز، سرور ٹائم اسٹامپ، انٹیگریٹیڈ سیکیورٹی رولز۔
مرحلہ 1 : فائر بیس پروجیکٹ بنائیں
ابتدائی کردار
Firebase CLI میری مشین پر انسٹال نہیں ہے، لہٰذا میں ویب کنسل استعمال کر رہا ہوں :
-
سپر جائیںhttps://console.firebase.google.com/[فائربیز کنسول]
-
پروجیکٹ بنائیں`cheroliv-contact`(یا موجودہ پروژے کو دوبارہ استعمال کریں)
-
Firestore کو نیٹو موڈ میں فعال کریں (Datastore نہیں)
-
منطقہ میں ایک ڈیٹا بیس بنائیں`eur3`(یورپ)
ہماری طرح کے سادہ استعمال کے لیے (ایک ہی جمع،عامی لکھاوت) نیٹیو مود مناسب اختیار ہے۔ Datastore کے پیچیدہ قوانین کی ضرورت نہیں۔
فائر سٹور امنیتی قواعد
فارم عام ہے — کوئی بھی پیغام بھیج سکتا ہے۔ مگر میں غلط استعمال کو محدود کرنا چاہتا ہوں۔
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contact_messages/{messageId} {
// Lecture : admin uniquement (authentifié)
allow read: if request.auth != null;
// Écriture : publique, mais limitée
allow create: if request.auth == null
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.name.size() <= 100
&& request.resource.data.email is string
&& request.resource.data.email.matches('.*@.*\\..*')
&& request.resource.data.email.size() <= 254
&& request.resource.data.subject is string
&& request.resource.data.subject.size() >= 3
&& request.resource.data.subject.size() <= 200
&& request.resource.data.message is string
&& request.resource.data.message.size() >= 10
&& request.resource.data.message.size() <= 5000
&& request.resource.data.created_at == request.time
&& request.resource.data.user_agent is string
&& request.resource.data.user_agent.size() <= 500;
}
}
}
اہم نکات :
-
allow read— صرف تصدیق شدہ کاربران پیامات کو پڑھ سکتے ہیں (میں، فایربیز کنسول کے ذریعے) -
allow create— کسی بھی ایک دستاویز بنا سکتا ہے، مگر فیلڈ کی تصدیق کے ساتھ -
серورزائی تصدیق : حداقل/حداکثر سائز، ای میل کا فارمٹ,
created_at`مطابق ہونا چاہیے`request.time(جعلی سازی کے خلا̱ف) -
`user_agent`یہ ٹریسبلٹی کے لیے بھیج دی جا رہی ہے (اہم نہیں لیکن مفید)
یہ قواعد سادہ سے زیادہ سخت ہیں`allow write: if true;`. وہ ایک هاجم کو زیادہ بڑے پیلوڈز یا غلط شکلے فیلڈز انجیکٹ کرنا سے روکتی ہیں
فاز 2 : Submission جاواسکریپٹ کو دوبارہ لکھیں
معاہدہ سادہ ہے :
-
فارم کے ڈیٹا پڑھیں
-
جانچ کریں ہانی پوٹ (فیلڈ`hp_name`— اگر یہ پر ہے تو یہ ایک bot ہے، ہم کامیابی کا schimit کر رہے ہیں بغیر کچھ بھیجے)
-
بلانا`addDoc(window.FIREBASE.collection(db, "contact_messages"), {…})`
-
کامیابی یا خطا دکھائیں
اعتمادیتی `window.FIREBASE
میں`footer.thyme`، ایک سکرپٹ ماڈیول فائر بیس SDK کو ایٹھل کرتا ہے اور ایک گلوبل آبجیکٹ کو انکشاف کرتا ہے :
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-app.js";
import { getFirestore, collection, addDoc, serverTimestamp }
from "https://www.gstatic.com/firebasejs/11.6.0/firebase-firestore.js";
const firebaseConfig = { /* valeurs réelles */ };
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
window.__FIREBASE__ = { db, collection, addDoc, serverTimestamp };
</script>
ماڈیول اسکرپٹس پہلے سے چلتے ہیں`DOMContentLoaded`, اس`window.FIREBASE`ہینڈلر کے وقت یہ گارنٹی سے قابل دستیاب ہے`contact.js`احتیاط کے طور پر، میں اب بھی 5 سیکنڈ کا polling شامل کر دیتا ہوں، اگر CDN slow ہو جائے۔
نیا `contact.js
document.addEventListener('DOMContentLoaded', function () {
'use strict';
const form = document.getElementById('contact-form');
if (!form) return;
const submitButton = form.querySelector('button[type="submit"]');
const successMessage = document.getElementById('contact-success-message');
const errorMessage = document.getElementById('contact-error-message');
// Éléments de validation
const nameInput = form.querySelector('input[name="name"]');
const emailInput = form.querySelector('input[name="email"]');
const phoneInput = form.querySelector('input[name="phone"]');
const subjectInput = form.querySelector('input[name="subject"]');
const messageInput = form.querySelector('textarea[name="message"]');
const honeypotInput = form.querySelector('input[name="hp_name"]');
/**
* Attend que window.__FIREBASE__ soit disponible.
* Timeout de 5 secondes — si le CDN Firebase est lent, on abandonne.
*/
function waitForFirebase(timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (window.__FIREBASE__) {
resolve(window.__FIREBASE__);
return;
}
const start = Date.now();
const interval = setInterval(() => {
if (window.__FIREBASE__) {
clearInterval(interval);
resolve(window.__FIREBASE__);
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Firebase SDK non disponible après timeout'));
}
}, 100);
});
}
// --- Validation (identique à l'existant) ---
function validateForm() {
nameInput.setCustomValidity('');
emailInput.setCustomValidity('');
if (phoneInput) phoneInput.setCustomValidity('');
subjectInput.setCustomValidity('');
messageInput.setCustomValidity('');
if (nameInput.value.trim().length < 1) {
nameInput.setCustomValidity('Veuillez saisir votre nom.');
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailInput.value.trim())) {
emailInput.setCustomValidity('Veuillez saisir une adresse email valide.');
}
if (phoneInput && phoneInput.value.trim() !== '') {
const phonePattern = /^\d{10,15}$/;
if (!phonePattern.test(phoneInput.value.trim())) {
phoneInput.setCustomValidity('Veuillez saisir un numéro valide (10 à 15 chiffres).');
}
}
if (subjectInput.value.trim().length < 3) {
subjectInput.setCustomValidity('Veuillez saisir un sujet (3 caractères minimum).');
}
if (messageInput.value.trim().length < 10) {
messageInput.setCustomValidity('Veuillez saisir un message (10 caractères minimum).');
}
form.classList.add('was-validated');
return form.checkValidity();
}
// --- Handler de soumission ---
form.addEventListener('submit', async function (event) {
event.preventDefault();
event.stopPropagation();
if (!validateForm()) return;
// Honeypot : si rempli, simuler un succès sans rien envoyer
if (honeypotInput && honeypotInput.value.trim() !== '') {
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
return;
}
// UI : état d'envoi
submitButton.disabled = true;
submitButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Envoi en cours...
`;
successMessage.style.display = 'none';
errorMessage.style.display = 'none';
try {
const fb = await waitForFirebase();
const messagesCollection = fb.collection(fb.db, 'contact_messages');
await fb.addDoc(messagesCollection, {
name: nameInput.value.trim(),
email: emailInput.value.trim(),
phone: phoneInput ? phoneInput.value.trim() : '',
subject: subjectInput.value.trim(),
message: messageInput.value.trim(),
created_at: fb.serverTimestamp(),
user_agent: navigator.userAgent.substring(0, 500)
});
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
} catch (error) {
console.error('Erreur Firestore:', error);
errorMessage.style.display = 'block';
} finally {
submitButton.disabled = false;
submitButton.innerHTML = `
<i class="bi bi-send me-2"></i>
Envoyer le Message
`;
}
}, false);
});
موک کے مقابلے میں تبدیلیاں :
-
waitForFirebase()— polling کے ساتھ timeout، مضبوط، اگرچہ CDN سست ہے -
honeypot— اگر خفیہ میدان`hp_name`بhar gaya hai, ایک بے شک کامیابی Firestore کاシュمیشن۔ بوت سوچتا ہے کہ وہ کامیاب ہوا ہے، لیکن کچھ بھی محفوظ نہیں ہے۔ -
addDoc(collection, {…})— سچا Firestore کال کے ساتھ`serverTimestamp()` etuser_agent -
خطأ کی منجمنٹ کے ساتھ`try/catch`ناہموار
-
صافی کرنے کا`finally`(بٹون کی بحالی)
|
کیوں`user_agent`? یہ اختیاری ہے، لیکن تشخیص کے لیے مفید ہے۔ اگر ایک عجیب پیغام آتا ہے، جاننا کہ یہ ڈیسک ٹاپ براؤزر، موبائل یا cURL اسکرپٹ سے آیا ہے، درج بندی میں مدد کرتا ہے۔ |
فاز 3 : Supabase کے مورت کوڈ کو صاف کریں
`script.js`یہ 250 لائنوں کا مردود کوڈ :
-
SupabaseManager(خط 417-481) — 65 خط -
ContactFormHandler(خطیں 490-551) — 62 خطوط -
ابتدائی بلاک (خط 645 سے 654 تک) — 10 líneas
کل: ~140 سطروں کو حذف کریں۔
بلاک`DOMContentLoaded`بنتا ہے ایک`SupabaseManager`ثم ایک`ContactFormHandler`فارم سے منسلک. جس طرح اوپر وضاحت کی گئی، یہ کد کبھی نہیں چلتا (روک دیا گیا`contact.js`), aur bhī agar woh chal raha hota, us mein nākāmī hoti (Supabase SDK load nahi hua).
میں حذف کرتا honour :
-
کلاس`SupabaseManager`
-
کلاس`ContactFormHandler`
-
ابتداء بلوک میں`DOMContentLoaded`(خطوط 645-654)
Le reste de script.js`سالم ہے :`ThemeManager, ScrollToTopButton, MobileMenuManager, SmoothScrollWithOffset, NavbarHeightUpdater, DynamicNavbarBreakpoint, CodeBlockManager, TooltipManager, PhoneInputManager.
فاز 4 : فوٹر کو ترتیب دیں
`footer.thyme`پہلے ہی Firebase boilerplate موجود lecturer چیز placeholder values کے ساتھ. میں بدلتا ہوں :
const firebaseConfig = {
apiKey: "REMPLACER_PAR_VOTRE_API_KEY",
authDomain: "REMPLACER_PAR_VOTRE_AUTH_DOMAIN",
projectId: "REMPLACER_PAR_VOTRE_PROJECT_ID",
storageBucket: "REMPLACER_PAR_VOTRE_STORAGE_BUCKET",
messagingSenderId: "REMPLACER_PAR_VOTRE_SENDER_ID",
appId: "REMPLACER_PAR_VOTRE_APP_ID"
};
حقیقی قدرات سے جو کے سے حاصل کی گئیپروجیکٹ سٹنگز > عام > آپ کی ایپلیکیشنز > ویب ایپفایربیز کンソول میں.
قیمتیں حساس ہیں`apiKey`Firebase کے ساتھ ڈیزائن سے عام ہے، لیکن میں انہیں واضح طور پر) کامٹ نہیں کرنا چاہتا ہوں۔ میں انہیں`site.yml`(پہلے سے`.gitignore`) اور پلاگین`bakery`انہیں بنڈ ساید پر شامل ہونے والی logique کے ذریعے ٹیمپلیٹ میں داخل کر دیتے ہیں۔
ابھی تک، میں انہیں بے کسی درمیانہ ڈالتا ہوں`footer.thyme`— بنیاد`./gradlew serve`یہ انہیں مقامی طور پر لوڈ کرے گا۔ ڈپلویمنت پر، میں انجیکشن کو جانب منتقل کروں گا۔`site.yml`یا گریڈیل متغیر کی طرف.
|
L'`apiKey`Firebase نہیںقدمایک راز. یہ ڈیزائن کے مطابق public ہے. جو آپ کے ڈیٹا کو حفاظت دیتا ہے، وہفائرستور سیکیورٹی کے قواعد, API کلید نہیں۔ اسے ایک میں نہ رکھیں`.env`Server-side loaded — یہ براؤزر پر نمائاں کرنے کے لئے تیار ہے۔ |
مرحلہ 5 : خاتمہ شدہ معماری
@startuml
skinparam backgroundColor #FEFEFE
title Architecture Finale — Formulaire de Contact Firebase
actor Utilisateur as USER
package "Navigateur" #E8F5E9 {
rectangle "contact.thyme\n(HTML, honeypot)" as FORM
rectangle "contact.js\n(validation + Firestore)" as CONT
rectangle "footer.thyme\n(Firebase SDK init)" as FOOT
}
cloud "Firebase" #BBDEFB {
rectangle "Firestore\n(contact_messages)" as FS
}
FORM --> CONT : submit event
CONT --> FOOT : window.__FIREBASE__.addDoc()
FOOT --> FS : Insert document\n(règles de sécurité validées)
note right of FS
Firestore Rules :
- create: public, validé
- read: auth uniquement
end note
@enduml
یہ ماگریش dogfooding کے بارے میں کیا کہتی ہے
یہ سائٹ میرا اپنا Gradle پلاگین بنانے سے بنائی گئی ہے`bakery`. رابطہ فارم سائٹ کے اندر ہے. Supabase سے Firebase تک کی منقلی میں دستاویز کردہ`AGENT.adoc`, یہ بیک لوگ میں مباحثہ کی جاتی ہے، یہ وے ذریعے آزمائش کی جاتی ہے`./gradlew serve`, Aur ye ek blog ka muzmun banati hai (jo aap parh rahe hain).
یہ صاف dogfooding ہے۔ سائٹ پلاگائن کا مَוצר ہے، پلاگائن ڈویلپر کا مَוצר ہے، ڈویلپر سائٹ خود پر عمل کی دستاویزات بناتا ہے۔
لوپ بند ہو گئی۔
ایک موک کو ماهوں تک کھینچنا (پوری سیسنز جہاں فارم خاموشی سے جھوٹ بول رہا تھا) مجھے ایک چیز محسوس ہوئی: ایک فردوسی ستے سٹی کا بیکلاگ کبھی مکمل نہیں ہوتا. ہمیشہ ایک پراہلٹی یوزر سٹوری ہوتی ہے، ہمیشہ ایک مضبوط ڈرافٹ میں، ہمیشہ ایک ٹیمپلیٹ میں تبصرہ شدہ سیکشن ہوتا ہے. ڈسپلائن کا مطلب یہ نہیں ہے کہ سب چیزیں مکمل کر دیں — بلکہ یہ ہے کہ جو استعمال کرنے والا دیکھ سکتا ہے، اسے مکمل کر دیں۔
ایک گڑنا ہوا رابطہ فارم، یہ کسی بھی فارم نہ ہونے سے بھی بد ہے۔ یہ ایک ناپورا کیا گیا وعدہ ہے۔
تغییرات کا خلاصہ
| فائل | تعدیل | اثر |
|---|---|---|
|
مقال کی تخلیق |
دوكومينٹيشن |
|
دوبارہ لکھائی (mock → Firestore واقعی) |
کام کرنے والا |
|
حذف SupabaseManager + ContactFormHandler + init bloc |
صفائی |
|
config placeholder کی بدلے → حقیقی قيمت |
ترکیب |
اگلے مراحل (backlog)
-
نوٹیفکیشن ایمیل: ایک کلاؤڈ فنکشن`onCreate`پر`contact_messages`جو SendGrid کے ذریعے ایک ایمیل بھیجتا ہے۔ فارم سٹور کرتا ہے، لیکن مجھے notification نہیں ملتی۔ میانی اولویت — پیغامز فائر بیس کنسول میں مرئی ہیں۔
-
کلائنٹ طرف ریٹ لیمیٹنگ: لوکل اسٹوریج ٹائم اسٹیمپ شامل کریں تاکہ کثیر بار فーム submissions کو روکنا۔ ہنی پٹ نئیب بٹس کو بلاک کرتا ہے، ایک رेट لی미ٹر ذہین بٹس کو بھی بلاک کر سکتا ہے۔
-
امتحانات: ایک Playwright ٹیسٹ جو فارم submit کرتا ہے اور جانچتا ہے کہ دستاویز Firestore میں ظاہر ہوتا ہے۔ ابھی تک، میں دستیابی سے ٹیسٹ کر رہا ہوں via`./gradlew serve`.