Libriq/libriq-admin
Admin dashboard for Libriq—send library alerts and manage settings. Astro + React + Tailwind with Firebase Auth/RTDB/Functions & FCM.
Libriq Admin
Admin web app for Libriq — a university library alerting system. Admins can sign in, draft announcements, and send push notifications to student apps. Built with Astro (TypeScript) + React islands, TailwindCSS v4, and Firebase (Auth, Realtime Database, Hosting, Cloud Functions).
Demo
- Live demo: Libriq Admin Demo
- Credentials: Contact me for a full demo with credentials
Features
- Astro + React islands UI
- Tailwind v4 styling via
@tailwindcss/vite(no config files needed) - Firebase Auth (email/SSO-ready) with custom claims for roles
- Realtime Database (RTDB) for settings & alerts
- Cloud Functions for privileged actions (e.g.,
sendAlertto FCM topic) - i18n-ready routing (e.g.,
/en/...,/fr/...)
Prerequisites
-
Node.js 18+ (Node 20 recommended)
-
Firebase CLI:
npm i -g firebase-tools -
A Firebase project with:
- A Web App (</>),
- Realtime Database created,
- Hosting enabled,
- (Later) Cloud Messaging for device push.
Getting Started
1) Install & run
# clone this repo
npm install
npm run devVisit the local URL printed by Astro.
2) Environment variables
Create a .env file in the project root:
PUBLIC_FB_API_KEY=...
PUBLIC_FB_AUTH_DOMAIN=...
PUBLIC_FB_DB_URL=https://<your-db>.firebasedatabase.app
PUBLIC_FB_PROJECT_ID=...
PUBLIC_FB_APP_ID=...
# optional
PUBLIC_FB_MESSAGING_SENDER_ID=...
PUBLIC_FB_MEASUREMENT_ID=...
Where to find these: Firebase Console → Project settings → General → Your apps (Web) → SDK setup & configuration.
PUBLIC_FB_DB_URL appears after you create Realtime Database: Build → Realtime Database → Data.
In Astro, only env vars prefixed with
PUBLIC_are exposed to the browser.
3) Point this repo to your Firebase project
firebase login
firebase use --add # pick your existing project and give it an alias (e.g., prod)Project Structure
src/
components/
NavBar.tsx
NavButton.tsx
islands/
AdminApp.tsx
RequireAuth.tsx (optional)
layouts/
BaseLayout.astro
pages/
en/
home/index.astro
...
login.astro
lib/
firebase.client.ts
useAuth.ts (optional hook)
styles/
global.css
functions/
src/index.ts (Cloud Functions)
firebase.json
.database.rules.json
.env.example
Firebase pieces
Auth (client)
Initialize once on the client and subscribe to auth state changes.
// src/lib/firebase.client.ts
import { initializeApp, type FirebaseApp } from 'firebase/app';
import { getAuth, browserLocalPersistence, setPersistence } from 'firebase/auth';
import { getDatabase } from 'firebase/database';
import { getFunctions } from 'firebase/functions';
let app: FirebaseApp | undefined;
export function getFb() {
if (!app) {
app = initializeApp({
apiKey: import.meta.env.PUBLIC_FB_API_KEY,
authDomain: import.meta.env.PUBLIC_FB_AUTH_DOMAIN,
databaseURL: import.meta.env.PUBLIC_FB_DB_URL,
projectId: import.meta.env.PUBLIC_FB_PROJECT_ID,
appId: import.meta.env.PUBLIC_FB_APP_ID,
});
setPersistence(getAuth(app), browserLocalPersistence);
}
return { app, auth: getAuth(app), db: getDatabase(app), fun: getFunctions(app) };
}Minimal check in a React island:
// src/lib/useAuth.ts
import { useEffect, useState } from 'react';
import { getFb } from './firebase.client';
import { onAuthStateChanged, type User } from 'firebase/auth';
export function useAuth() {
const { auth } = getFb();
const [user, setUser] = useState<User | null | undefined>(undefined);
useEffect(() => onAuthStateChanged(auth, setUser), [auth]);
return { user, loading: user === undefined };
}Roles (custom claims)
Set once with the Admin SDK (script or locked-down function):
await admin.auth().setCustomUserClaims(uid, { role: 'admin', universityId: 'dal' });Clients can read claims via getIdTokenResult(user, true).
Realtime Database (schema idea)
/universities/{univId}/settings { name, colors, logoUrl, ... }
/universities/{univId}/alerts/{alertId} { title, body, status, createdAt, createdBy }
/users/{uid}/deviceTokens/{token}: true # optional audit/targeting
RTDB Security Rules
database.rules.json:
{
"rules": {
".read": false,
".write": false,
"universities": {
"$univId": {
"settings": {
".read": true,
".write": "auth != null && auth.token.role === 'admin' && auth.token.universityId === $univId"
},
"alerts": {
".indexOn": ["createdAt", "status"],
"$alertId": {
".read": true,
".write": "auth != null && auth.token.role === 'admin' && auth.token.universityId === $univId"
}
}
}
},
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
}
}
}Deploy rules:
firebase deploy --only databaseCloud Functions (privileged actions)
functions/src/index.ts sample for sending topic alerts:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const sendAlert = functions.https.onCall(async (data, context) => {
if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Sign in required.');
const role = context.auth.token.role as string | undefined;
const universityId = context.auth.token.universityId as string | undefined;
if (role !== 'admin' || !universityId) throw new functions.https.HttpsError('permission-denied', 'Admin only.');
const { title, body, data: extra } = data ?? {};
if (!title || !body) throw new functions.https.HttpsError('invalid-argument', 'title/body required.');
const id = await admin.messaging().send({
notification: { title, body },
data: { ...extra, universityId },
topic: `univ_${universityId}`
});
await admin.database().ref(`universities/${universityId}/alerts`).push({
title, body, status: 'sent', sentAt: Date.now(), sentBy: context.auth.uid
});
return { id };
});Deploy functions:
cd functions && npm i && cd ..
firebase deploy --only functionsTailwindCSS v4
This project uses Tailwind v4 via the Vite plugin (no tailwind.config.*).
astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
integrations: [react()],
vite: { plugins: [tailwindcss()] }
});src/styles/global.css
@import "tailwindcss";
/* Optional project tokens (Tailwind v4) */
@theme {
--color-background: #F7F7F8;
--color-foreground: #111111;
--color-brand: #3A7AFE;
}
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";Install optional plugins:
npm i -D @tailwindcss/forms @tailwindcss/typographyImport once in your base layout:
---
import "../styles/global.css";
---If you prefer the classic Tailwind setup with
tailwind.config.mjs, you can switch tonpx astro add tailwindand remove the Vite plugin.
i18n routing
- Pages are organized under language folders:
src/pages/en/...,src/pages/fr/.... - A helper like
getLangFromUrl(Astro.url)can derive the language segment. - You can add Hosting redirects for convenience (e.g.,
/en→/en/home/).
firebase.json
{
"hosting": {
"public": "dist",
"predeploy": ["npm run build"],
"redirects": [
{ "source": "/en", "destination": "/en/home/", "type": 301 },
{ "source": "/en/", "destination": "/en/home/", "type": 301 }
]
}
}Deployment
One-time setup
- Ensure
firebase.jsonuses"public": "dist". - Ensure scripts in
package.json:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"deploy": "firebase deploy"
}
}Deploy
npm run build
firebase deploy --only hostingPreview channels (temporary URLs):
firebase hosting:channel:deploy stagingTroubleshooting
- Seeing the Firebase starter page →
firebase.jsonlikely points topublic/. Use"public": "dist"and redeploy. - Tailwind styles not applying → Import
src/styles/global.cssin a layout; restart dev server. - Auth appears as logged-out briefly → Seed state from
auth.currentUserand subscribe withonAuthStateChanged. - RTDB
PERMISSION_DENIED→ Ensure your user has claims{ role: 'admin', universityId: '<id>' }and rules deployed. - Callable function
unauthenticated→ You must be signed in; the client SDK passes the ID token automatically.
Scripts
npm run dev— start dev servernpm run build— build for production (outputs todist/)npm run preview— preview the prod buildfirebase deploy --only hosting|functions|database— deploy pieces
Notes
- Firebase web config keys (apiKey, authDomain, etc.) are not secrets. Access is enforced by Security Rules and Functions auth checks.
- Do not commit service account JSON or
.envfiles. Use.env.exampleto share variable names.
Roadmap (ideas)
- Admin user management (set roles via UI)
- Draft/schedule alerts; templates and targeting by topic
- Per-university runtime theming (load CSS variables from RTDB)
- Analytics & delivery reports