Platform Architecture
This architecture snapshot reflects the code that exists in the tank-wise-portal repository today. It omits aspirational services that do not yet exist in source control.
High-level viewβ
βββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β GitHub (tank-wise-portalβββββββ€ Feature / PR branches β
ββββββββββββ¬βββββββββββββββ ββββββββββββ¬ββββββββββββββββ
β CI triggers β
ββββββββββββΌβββββββββββββββ ββββββββββββββΌββββββββββββββ
β Vercel Preview Deploys ββββββΊβ Vercel Production Deploy β
ββββββββββββ¬βββββββββββββββ ββββββββββββββ¬ββββββββββββββ
β β
ββββββββββββΌβββββββββββββββ ββββββββββββββββΌββββββββββββ
β TankSafe Portal β β Supabase project β
β β’ React (Vite) β β β’ Auth (email/password) β
β β’ TypeScript β β β’ Postgres tables β
β β’ Tailwind + Radix UI β β β’ Storage & edge functionsβ
βββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
There is no separate API gateway or worker tier. React components call Supabase directly from the browser.
Front-end structureβ
- Entry points β
src/main.tsxbootstraps React Router and wraps the app in providers (QueryClientProvider,AuthProvider,TooltipProvider). - Routing β
src/App.tsxdeclares all routes.ProtectedRouteguards authenticated areas and enforces administrator-only routes via theadminOnlyprop. - Layout β
AppLayoutrenders the navigation shell (sidebar, header, toast containers) used across the authenticated UI. - Styling β Tailwind CSS with utility classes in JSX, plus shared styles in
src/index.cssandsrc/App.css. - State β React hooks and React Query handle most state.
useSharedWizardFieldskeeps cross-step data in sync inside the inspection wizard.
const App = () => (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/auth" element={<Auth />} />
<Route
path="/"
element={
<ProtectedRoute>
<AppLayout>
<Index />
</AppLayout>
</ProtectedRoute>
}
/>
{/* additional routes trimmed for brevity */}
</Routes>
</BrowserRouter>
</TooltipProvider>
</AuthProvider>
</QueryClientProvider>
);
Supabase integrationβ
- Auth β Supabase email/password authentication with optional TOTP MFA.
useAuth.tsxwraps session handling, profile loading, and MFA state. - Data β The app queries Supabase tables directly (e.g.,
inspections,form_responses,inspection_checklist_items,continuity_test_readings). Helper utilities insrc/lib/api.ts,src/lib/gridConfigs.ts, andsrc/lib/formSchemas.tscentralise repetitive logic. - Edge functions β Located under
supabase/functions/admin-*. These functions perform privileged user-management tasks (invite, update, delete, list MFA status). No other server-side automation is present. - Storage β Thickness test drawings save annotated images to Supabase Storage via URLs captured in
thickness_test_records. There is no general asset pipeline beyond this use case.
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [hasVerifiedMfa, setHasVerifiedMfa] = useState(false);
useEffect(() => {
const applySession = async (session: Session | null) => {
if (session?.user) {
setSession(session);
setUser(session.user);
const userProfile = await fetchProfile(session.user.id);
setProfile(userProfile);
await evaluateMfaStatus();
return;
}
setSession(null);
setUser(null);
setProfile(null);
setHasVerifiedMfa(false);
setRequiresMfaSetup(false);
};
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
setSession(null);
setUser(null);
setProfile(null);
setIsLoading(false);
return;
}
applySession(session ?? null);
});
supabase.auth.getSession().then(({ data: { session } }) => applySession(session ?? null));
return () => subscription.unsubscribe();
}, []);
return (
<AuthContext.Provider value={{ user, session, profile, hasVerifiedMfa, /* β¦ */ }}>
{children}
</AuthContext.Provider>
);
}
serve(async (req) => {
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: corsHeaders });
}
const supabaseAdmin = createClient(supabaseUrl, serviceRoleKey, {
auth: { persistSession: false, autoRefreshToken: false },
});
const inviteResult = await supabaseAdmin.auth.admin.inviteUserByEmail(email, {
data: { full_name },
});
if (role === "inspector") {
const { data: existingInspector } = await supabaseAdmin
.from("inspectors")
.select("id")
.eq("user_id", userId)
.maybeSingle();
if (!existingInspector) {
await supabaseAdmin.from("inspectors").insert({
user_id: userId,
name: full_name,
});
}
}
return new Response(JSON.stringify({ message: "Invite sent", userId, role }), {
status: 200,
headers: corsHeaders,
});
});
Key modulesβ
InspectionWizard.tsxβ Implements the multi-step inspection workflow defined by theWIZARD_STEPSconstant. Relies onJsonForm,GridTable,ContinuityMatrix, andThicknessTestCanvascomponents.AdminJobDetail.tsxβ Aggregates job metadata, form responses, checklist items, continuity readings, and charge sheet data into a dossier-style view.AdminJobEdit.tsxβ Provides full edit access to inspection details and supporting tables using shared form/grid components.AdminSchedule.tsxβ Usesreact-big-calendarwith drag-and-drop to manage inspection schedules, inspector availability, and reminders.AdminUsers.tsxβ Orchestrates edge function calls for inviting, updating, deleting users, and displaying MFA status.
const WIZARD_STEPS = [
{
key: 'risk',
title: "Risk Assessment",
subtitle: "Permit to Work",
templateId: TEMPLATE_IDS.RISK_ASSESSMENT,
hasGrid: true,
gridTable: 'risk_assessment_compartments',
},
{
key: 'vt_header',
title: "Vapour Tightness",
subtitle: "Header & Page 1",
templateId: TEMPLATE_IDS.VAPOUR_TIGHTNESS,
hasGrid: true,
gridTable: 'vapour_tightness_compartments',
},
// β¦additional steps omitted for brevity
];
const InspectionWizard = () => {
const { job } = useParams();
const [currentStep, setCurrentStep] = useState(0);
const currentStepConfig = WIZARD_STEPS[currentStep];
const defaultValues = useMemo<JsonObject>(() => {
const savedData = formData[currentStepConfig.templateId] ?? {};
return {
...initialFormDefaults,
...sharedData,
...Object.fromEntries(Object.entries(savedData).filter(([, value]) => isMeaningfulValue(value))),
};
}, [currentStepConfig, formData, initialFormDefaults, sharedData]);
// β¦wizard logic continues
};
Database overviewβ
Refer to the SQL migrations under supabase/migrations for the authoritative schema. The UI actively works with these tables:
inspectionsβ Core job/inspection record (status, scheduling, operator snapshot, inspector assignment).form_responsesβ Stores JSON form data keyed bytemplate_id.inspection_checklist_itemsβ Individual checklist entries linked to an inspection.continuity_test_readingsβ Electrical continuity measurements per compartment.charge_sheet_work_items,charge_sheet_parts,charge_sheet_labourβ Billing data summarised in Admin views.thickness_test_recordsβ Stores image URLs for annotated thickness diagrams.inspectors,profiles,inspector_availability,schedule_remindersβ Support assignment and scheduling workflows.
Row-level security policies are managed in Supabase and enforced automatically via the Supabase client; there is no custom policy bypass logic in the portal.
export interface Inspection {
id: string;
job_number: string;
inspection_number: string | null;
status: InspectionStatus;
operator_id: string | null;
inspector_id: string | null;
notes: string | null;
created_at: string;
updated_at: string;
}
export interface FormResponse {
id: string;
inspection_id: string;
template_id: string;
answers: JsonObject;
completed_at: string | null;
created_at: string;
}
Extensibility pointsβ
- Add new wizard steps by extending
WIZARD_STEPSand updatingFORM_SCHEMAS/GRID_CONFIGSaccordingly. - To introduce new admin tables, create the Supabase schema migration, extend query logic inside
AdminJobDetail.tsxorAdminJobEdit.tsx, and update helper utilities as needed. - Edge functions can be added under
supabase/functions. The front-end invokes them throughsupabase.functions.invokejust as the admin user functions do today.
Deployment surfaceβ
- Source control lives in GitHub. Day-to-day commits target the shared
developmentbranch; releases mergedevelopmentintomainvia pull request. - Vercel automatically builds every push to
development, providing preview URLs for QA and stakeholder review. Merging tomainpromotes the build to production without manual intervention. - The docs site (
docusaurus/tanksafe-docs) follows the same GitHub/Vercel pairing so product and documentation stay in sync. - Supabase environment variables (URL, publishable key, service role key) are managed in Vercel project settings. Keep preview and production values aligned before merging.
Keep this page updated whenever the repository adds new architectural pieces so the docs remain accurate. For a deeper dive into branch strategy, preview builds, and production rollouts, see Development & Deployment Workflow.