
Oracle Backend with Firebase APIs, Deel 2: Hands-on met authenticatie, database en opslag
Deel 2 van een tweedelige reeks over Oracle Backend with Firebase APIs: Deel 1 - Wat het is en hoe je het opzet · Deel 2 - Hands-on met authenticatie, database en opslag.
In Deel 1 installeerden we Fusabase, maakten we met de quickstart een project aan en eindigden we met een node index.js die { appName: '[DEFAULT]', type: 'oracledb' } afdrukte. Leuk, maar niemand brengt een console.log naar productie.
Deze keer bouwen we iets echt: een kleine todo-app, verdeeld over demo’s die aansluiten bij de belangrijkste services, met realtime als finale:
- Authenticatie - registreren, aanmelden en inloggen met Google
- Database - een persoonlijke takenlijst in documentcollecties
- Opslag - bestandsbijlagen per gebruiker
- Realtime - dezelfde lijst, tegelijk live op twee apparaten
Alle code staat op GitHub: github.com/vito-vanhecke/fusabase-demo. Bij de eerste drie demo’s volgen we telkens hetzelfde patroon: probeer iets vanuit de app, bots op een blokkade, los die op in de Console en ga daarna terug naar de Console om te zien wat er is gebeurd. Bijna niets werkt bij de eerste poging, en juist dat blijkt het beste deel van het verhaal te zijn.
De demo-app
De repository is een eenvoudig Vite-project met vier losstaande demomappen en één gedeelde configuratie:
fusabase-demo/
├── fusabase-config.js # de appconfiguratie uit Deel 1
├── demos/
│ ├── 01-auth/ # demo 1: authenticatie
│ ├── 02-database/ # demo 2: de takenlijst
│ ├── 03-storage/ # demo 3: bijlagen
│ └── 04-realtime/ # demo 4: live synchronisatie met onSnapshot
├── rules/ # de beveiligingsregels die we publiceren
│ ├── database.rules
│ └── storage.rules
└── scripts/screenshots.mjs # maakt alle appscreenshots in deze post opnieuw
git clone https://github.com/vito-vanhecke/fusabase-demo.git
cd fusabase-demo && npm install && npm run dev

Elke demo begint op dezelfde manier, met het configuratieobject dat de Console in Deel 1 voor de app WEBDEMO heeft aangemaakt:
// fusabase-config.js
export const fusabaseConfig = {
schema: "vito",
app_name: "WEBDEMO",
app_type: "WEB",
app_id: "58D64B0C789F4307E063F40D1FAC19AD",
objs_type: "dbfs",
project_id: "58D6470B91BF4302E063F40D1FAC8719",
storage_bucket: "dbfs_YALOWOGCQGWKBQN",
auth_type: "base",
auth_id: "58D6470B91C34302E063F40D1FAC8719",
ords_host: "https://oracle.vvanhecke.be/ords/vito/",
};
Ja, dat staat allemaal in een publieke repository, en nee, dat is geen lek. Net als een Firebase-configuratieobject is dit configuratie aan de clientzijde: elke browser die de app laadt, krijgt deze gegevens sowieso. De toegangscontrole komt van authenticatie en beveiligingsregels, en daar gaat deze post precies over.
Demo 1: Authenticatie
Configureer het in de Console
De eerste keuze is het authenticatietype, één per project: BASIC, LDAP of IDCS. Dit bepaalt gewoon waar de gebruikersaccounts worden opgeslagen. BASIC bewaart ze in een gewone tabel in je eigen databaseschema. LDAP gebruikt een bestaande bedrijfsdirectory, waar bijvoorbeeld de logins van medewerkers al in staan. IDCS laat het aanmelden over aan Oracles cloudidentiteitsdienst.

Op deze pagina vind je ook het wachtwoordbeleid (een lengte van 6 tot 30 tekens en de gebruikelijke opties zoals “moet een cijfer of symbool bevatten”) en de instellingen van je uitgaande e-mailserver (SMTP). Zonder geconfigureerde e-mailserver kan de app geen verificatie- of wachtwoordherstelmails versturen.
Ik heb ook inloggen met Google ingeschakeld. Dit is de standaardconfiguratie voor “Sign in with Google”: maak in de Google Cloud Console een client-ID en een geheim aan, geef Google de callback-URL die de Fusabase Console toont en plak daarna de client-ID en het geheim van Google in de Console:

Dat is alles. Google OAuth werkt net zoals in je APEX-applicaties.
Gebruik het in de app
import { initializeApp } from "fusabase/app";
import {
getAuth,
onAuthStateChanged,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signInWithPopup,
GoogleAuthProvider,
signOut
} from "fusabase/auth";
import { fusabaseConfig } from "../../fusabase-config.js";
const app = initializeApp(fusabaseConfig);
const auth = getAuth(app);
// wordt uitgevoerd bij aanmelden, afmelden EN bij het laden van een herstelde sessie
onAuthStateChanged(auth, (user) => {
console.log(user ? `aangemeld: ${user.uid}` : "afgemeld");
});
// e-mail/wachtwoord
await createUserWithEmailAndPassword(auth, email, password);
await signInWithEmailAndPassword(auth, email, password);
// Google
await signInWithPopup(auth, new GoogleAuthProvider());
await signOut(auth);
Demo 1 zet dit in een kleine pagina met een registratieformulier, een aanmeldformulier en een Google-knop:

Ik vulde dus een e-mailadres en wachtwoord in, klikte op Sign in, en kreeg dit:

auth/network-error. De ontwikkelaarsconsole van de browser wijst naar CORS, de ingebouwde browserregel die bepaalt dat een pagina alleen backends mag aanroepen die haar webadres uitdrukkelijk hebben goedgekeurd. De server bevestigt dat met fout ORDS-13002: “the request Origin is not authorized to access this resource.” Dit is de eerste blokkade die standaard alles weigert: aanmeldverzoeken werken alleen vanaf webadressen die je op een allowlist hebt gezet. Zo kan een willekeurige website niet gewoon je configuratie, het fusabaseConfig-object, kopiëren en gebruikers bij jouw backend aanmelden. http://localhost:5173 staat nog niet op die lijst, dus voor de backend is mijn lokale app gewoon een andere niet-vertrouwde website.
De oplossing staat in de Console onder Project Settings → Authorized Domains:

Daarna werkt het aanmelden:

Het onderste paneel toont wat er in het aanmeldtoken van de gebruiker zit. Dat token is een JWT (JSON Web Token): een ondertekend, fraudebestendig pakketje JSON dat de backend na het aanmelden terugstuurt. Het bevat een scope-veld dat precies aangeeft welke services deze gebruiker mag bereiken:
"idp_type": "BASE",
"sub": "demo@vvanhecke.be",
"aud": "58D6470B91BF4302E063F40D1FAC8719",
"scope": "baas-database baas-auth baas-storage"
Terug naar de Console
De gebruiker die de demo heeft aangemaakt, staat nu gewoon onder Authentication → Users, naast de accounts die via Google inloggen:

Demo 2: Database
Gebruik het in de app
De database gebruikt JSON als belangrijkste datastructuur. Alles wat met de database te maken heeft, is JSON. Het basisidee is eenvoudig: collecties bevatten documenten, en die documenten hebben velden met bijvoorbeeld getallen, tekst, booleans of datums.
Hier maken we onze collectie todos aan:
import {
getOracledb,
collection,
doc,
addDoc,
updateDoc,
deleteDoc,
getDocs,
query,
where,
orderBy
} from "fusabase/oracledb";
const db = getOracledb(app);
const todos = collection(db, "todos");
// aanmaken
await addDoc(todos, {
uid: user.uid,
title: "Schrijf deel 2 van de blogreeks",
done: false,
createdAt: Date.now(),
});
// lezen: alleen mijn taken, de nieuwste eerst
const snaps = await getDocs(
query(todos, where("uid", "==", user.uid), orderBy("createdAt", "desc"))
);
// bijwerken en verwijderen werken met een documentreferentie
await updateDoc(doc(db, "todos", id), { done: true });
await deleteDoc(doc(db, "todos", id));
Aan de Oracle-kant zijn dit JSON-collecties in het schema van de projecteigenaar. Schrijven naar een pad maakt de collectie aan als die nog niet bestaat. Laten we dus onze eerste taak toevoegen:

ORA-20015: Security rule not found, access denied. Blokkade nummer twee, met dezelfde filosofie als de eerste: geen regel betekent geen toegang. Er is geen testmodus die je gegevens voor de hele wereld openzet totdat je tijd vindt om ze te beveiligen.
Configureer het in de Console
Beveiligingsregels zijn kleine if-voorwaarden, geschreven in een regeltaal genaamd CEL (Common Expression Language, dezelfde taal die Firebase gebruikt). De backend voert bij elk verzoek de bijpassende regel uit. Dit is de volledige set regels voor de takenlijst (rules/database.rules in de repository):
match /todos/{todoId} {
// Elke aangemelde gebruiker mag een taak maken, maar alleen als zichzelf.
allow create: if request.auth != null
&& request.resource.data.uid == request.auth.uid;
// Je kunt alleen je eigen taken bekijken en wijzigen.
allow get, list: if request.auth != null
&& resource.data.uid == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
}
De termen: de methoden zijn get, list, create, update en delete. request.auth is het geverifieerde token, request.resource.data is het binnenkomende document en resource.data het opgeslagen document. Let op het verschil: create controleert de binnenkomende uid, terwijl de andere methoden de opgeslagen uid controleren. Daardoor zijn “je kunt alleen taken aanmaken als jezelf” en “je kunt alleen je eigen taken aanpassen” twee verschillende garanties.
De Console heeft een regeleditor met validatie en een simulator. Zo kun je een regel met een voorbeeldverzoek testen voordat je hem publiceert:

Als je uit de APEX-wereld komt, is dit de belangrijkste omschakeling: dit is niet Oracles Virtual Private Database (VPD, de beveiliging op rijniveau die je aan databasetabellen toevoegt), en het zijn ook geen databaserollen of grants. De regel wordt voor elk verzoek uitgevoerd en vergelijkt het binnenkomende verzoek met de opgeslagen rij. Het lijkt meer op een WHERE-clausule die je één keer schrijft dan op een permanente grant.
Er is nog een tweede configuratie die we hier best uitvoeren: indexen, de zoekstructuren waarmee een database snel rijen vindt. Onze query combineert where('uid', '==', …) met orderBy('createdAt', 'desc'). In mijn kleine democollectie werkte dat prima zonder index, maar Oracle raadt aan daar niet op te vertrouwen. Je kunt handmatig een index maken voor de velden die je opvraagt, of automatische indexen inschakelen zodat ze voor jou worden aangemaakt en onderhouden. Complexere queries, bijvoorbeeld queries die veel collecties tegelijk doorzoeken of collecties samenvoegen, hebben altijd een handmatig aangemaakte index nodig. Ik heb automatische indexen ingeschakeld:

En nu werkt het
De regels zijn gepubliceerd:

In de Console verscheen de collectie todos zodra de eerste addDoc slaagde. De documenten staan er als JSON, die je ook met gewone SQL kunt opvragen vanuit hetzelfde schema waarin je APEX-app leeft:

Twee functies bewaar ik nog even: de live listeners van onSnapshot krijgen hieronder hun eigen demo, en het zichtbaar maken van je bestaande relationele tabellen als documentcollecties verdient een eigen post. Dat laatste gebeurt via JSON duality views, een functie van 26ai die gewone tabellen als JSON-documenten toont en wijzigingen terug naar de tabellen schrijft. Precies dat maakt dit echt interessant voor een Oracle-omgeving met twintig jaar aan tabellen.
Demo 3: Opslag
Gebruik het in de app
De opslag gebruikt hier DBFS (Database File System): de geüploade bestanden staan letterlijk in de Oracle Database. Daarbovenop zit dezelfde API voor bestandsbeheer die Firebase aanbiedt. Onze todo-app geeft elke gebruiker een persoonlijke map voor bijlagen:
import {
getStorage,
ref,
uploadBytesResumable,
getDownloadURL,
listAll,
deleteObject
} from "fusabase/storage";
const storage = getStorage(app);
const fileRef = ref(storage, `attachments/${user.uid}/${file.name}`);
const task = uploadBytesResumable(fileRef, file, { contentType: file.type });
task.on("state_changed",
(snap) => console.log(`${(snap.bytesTransferred / snap.totalBytes * 100).toFixed(0)}%`),
(err) => console.error(err),
async () => console.log("klaar:", await getDownloadURL(task.snapshot.ref))
);
// toon de map van een gebruiker en ruim ze daarna op
const { items } = await listAll(ref(storage, `attachments/${user.uid}`));
await deleteObject(items[0]);
Een klein aandachtspunt dat ik tijdens het bouwen ontdekte: zet geen slash achteraan een mappad. ref(storage, 'attachments/uid/') faalt al aan de clientzijde met “Invalid child path”, nog voordat er een verzoek wordt verstuurd.
Je kent het patroon inmiddels. Upload een bestand, en:

Configureer het in de Console
Opslagregels gebruiken dezelfde structuur, maar met een iets andere woordenschat. Je koppelt regels aan paden in een bucket in plaats van aan documenten. Deze regel dwingt één ding af: je kunt alleen je eigen map gebruiken (rules/storage.rules):
match /attachments/{userId}/{fileName} {
// Alleen de eigenaar van de map mag bestanden tonen, downloaden, uploaden en verwijderen.
allow get, list: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null && request.auth.uid == userId;
allow delete: if request.auth != null && request.auth.uid == userId;
}

En nu werkt het

De Console toont het bestand in de persoonlijke DBFS-map van de gebruiker:

Demo 4: Realtime
Dit is de functie waarvan de ogen van een mobiele ontwikkelaar gaan glimmen. onSnapshot vervangt “één keer lezen” door “abonneren”: je geeft dezelfde query mee als bij getDocs, en je callback wordt opnieuw uitgevoerd telkens wanneer de onderliggende gegevens veranderen. Demo 4 plaatst twee onafhankelijke listeners voor dezelfde todos-query naast elkaar, als Apparaat A en Apparaat B:
import { onSnapshot, query, collection, where, orderBy } from "fusabase/oracledb";
const q = query(
collection(db, "todos"),
where("uid", "==", user.uid),
orderBy("createdAt", "desc")
);
// wordt nu uitgevoerd met de huidige gegevens, en daarna bij elke wijziging
const unsubscribe = onSnapshot(q, (snapshot) => {
render(snapshot.docs.map((d) => ({ id: d.id, ...d.data() })));
});
Voeg aan de ene kant een taak toe en die verschijnt vanzelf aan de andere kant: geen refresh en geen pollinglus in je code.

Tijd voor de eerlijkheid waarop deze reeks draait. onSnapshot kan updates op twee manieren ontvangen. Standaard gebruikt het long polling: de client vraagt op vaste tijdstippen opnieuw aan de server of er iets nieuws is. Die timer staat standaard op 30 seconden, waardoor “realtime” zonder aanpassingen dus “binnen een halve minuut” kan betekenen. Met long_polling_interval kun je die verlagen tot minimaal 5 seconden. Dat is wat de GIF hierboven gebruikt; als je goed kijkt, loopt het andere apparaat een tel achter:
const app = initializeApp({ ...fusabaseConfig, long_polling_interval: 5 });
De andere optie is een echte WebSocket (use_socket: true). Die houdt een verbinding open, zodat de server wijzigingen naar de app kan sturen zodra ze plaatsvinden.
Dat is tenminste hoe het hoort te werken. In mijn opstelling kreeg ik het niet goed werkend. De verbinding werd geopend en bleef open, maar aanvankelijk kwamen er geen wijzigingen door. De database detecteerde de wijzigingen en ORDS verwerkte ze, maar in de ORDS-logs stond No JWK State was identified to verify this JWT. Het leek erop dat ORDS het aanmeldtoken van de gebruiker niet kon verifiëren.
Heb ik een configuratiestap gemist? Ik kon er geen vinden in de documentatie. Nadat ik het onderstaande JWT-profiel handmatig had toegevoegd, kwamen nieuwe en bijgewerkte items wel door:
-- voer dit uit als het projectschema (de REST-enabled eigenaar)
BEGIN
OAUTH.CREATE_JWT_PROFILE(
p_issuer => 'baas_onprem#<auth_id>',
p_audience => '<project_id>',
p_jwk_url => 'https://<host>/ords/<schema>/_/baas-services/idm/signingKey/<project_id>/jwk'
);
COMMIT;
END;
/
Zelfs daarna voelde het niet helemaal realtime. Nieuwe en bijgewerkte items liepen meestal één of twee seconden achter, en verwijderingen kwamen in mijn tests helemaal niet via de WebSocket door. Die verschenen pas bij de volgende poll.
Misschien ligt dit aan mijn opstelling, of heb ik nog een configuratiestap gemist. Voor de demo gebruikte ik daarom long polling om de 5 seconden, omdat dat betrouwbaar werkte voor aanmaken, bijwerken en verwijderen. Ik hoor graag of anderen hetzelfde gedrag zien bij het gebruik van WebSockets door Fusabase.
De conclusie
De eerste drie demo’s botsten elk op dezelfde soort blokkade, met telkens één bezoek aan de Console:
| Je probeert | Het mislukt met | Je configureert |
|---|---|---|
| Aanmelden vanuit de webapp | ORDS-13002, origin niet geautoriseerd | Geautoriseerde domeinen |
| Een document schrijven | ORA-20015, geen beveiligingsregel | Databaseregels |
| Een bestand uploaden | ORA-20012, geen beveiligingsregel | Opslagregels |
Alles weigert hier standaard de toegang: zonder geautoriseerd domein kun je je niet aanmelden en zonder beveiligingsregel wordt je verzoek geweigerd. Frustrerend op dag één, precies goed op dag negentig. De configuratie die je verplicht moet schrijven, is je beveiligingsmodel. Er is nooit een periode waarin je er toevallig van uitgaat dat de standaardinstellingen je beschermen.
Dankzij de Firebase-vormige API erbovenop is de appcode zelf heerlijk voorspelbaar: vier kleine demo’s, waarbij alleen de twee eigenaardigheden van de opslagregels en het realtime transport echt verrasten. Alles staat in de demo-repository, klaar om te klonen en aan je eigen project te koppelen.