DEV Community

Israel Michael
Israel Michael

Posted on

UICS Connect: A Technical Walkthrough of the Alumni Partnership System

Login screen

The alumni-partnership-system is a networking platform for the University of Ibadan Computer Science community. It connects students and alumni through profiles, posts, communities, jobs, direct messaging, notifications, and connection requests. It is a Next.js 14 App Router application that pushes a lot of behaviour to the client, uses Supabase as both database and auth boundary, and relies on React Query to keep the UI coherent while data changes quickly.

A school community needs more than a directory. It needs identity, lightweight publishing, private conversation, and a way to move between social and career features without building separate systems. UICS Connect treats the authenticated user as the centre of every workflow: sign in, hydrate user state, fetch domain data, then keep the UI current with optimistic updates and realtime subscriptions.

Architecture and Boundaries

The repository has a clean split between routing, domain logic, and UI composition. app/ owns layouts and pages. components/ contains shared UI, feature components, and provider setup. services/ is the domain layer for Supabase queries, mutations, file uploads, and permission checks. hooks/ wraps those services with React Query and turns them into UI-friendly primitives.

Authentication is handled in two places. middleware.ts delegates to lib/supabase/middleware.ts, which refreshes the session and redirects users based on route type. It also sanitizes the login callback so the app only redirects internally:

if (!user && !isPublicRoute) {
  const url = request.nextUrl.clone();
  url.pathname = "/login";
  if (pathname !== "/") {
    url.searchParams.set("callbackUrl", pathname);
  }
  return NextResponse.redirect(url);
}
Enter fullscreen mode Exit fullscreen mode

That is paired with server-side auth actions in services/auth.service.ts, where login() calls redirect() only after checking callbackUrl.startsWith("/").

At the application shell level, components/providers/providers.tsx creates one QueryClient and wraps the tree with theme, push notification, keyboard command, and context providers. components/providers/context.tsx currently exposes an empty context object, which tells you what this codebase is not doing. Remote state lives in React Query. Local UI state stays local.

Data Flow and State Management

The dominant data flow looks like this: a component fires an event, a custom hook triggers a React Query mutation, the service module talks to Supabase, and the cache is updated optimistically or invalidated for refetch. If the feature needs live updates, a Supabase realtime channel patches the cache when rows change.

The post flow is the clearest example. app/(logged-in)/_components/post-composer.tsx collects content with TipTap, validates images, then calls useCreatePost(). That hook lives in hooks/use-posts.ts and delegates to createPost() in services/posts.service.ts. The same file also contains the more interesting mutations, especially optimistic updates for edits:

queryClient.setQueriesData({ queryKey: ["posts"] }, (old: any) => {
  if (!old?.pages) return old;

  return {
    ...old,
    pages: old.pages.map((page: any) => ({
      ...page,
      posts: page.posts.map((p: Post) =>
        p.id === postId ? { ...p, ...data, _optimistic: true } : p
      )
    }))
  };
});
Enter fullscreen mode Exit fullscreen mode

That pattern shows up all over the app. hooks/use-chats.ts uses the same approach for messages, including temporary blob URLs for attachment previews and rollback on failure. The upside is responsiveness. The cost is cache bookkeeping when a mutation affects both an infinite list and a detail view.

Remote state is mostly React Query state. Local state is reserved for ephemeral interaction details. app/(logged-in)/chat/components/chat-zone.tsx keeps replyingTo, editingMessage, stickyDate, and scroll state in component memory because those values are view-specific and do not belong in shared cache.

Interesting Implementation Details

The feed and comment system carries some of the best engineering work in the repo. getPostComments() in services/posts.service.ts builds a nested tree in O(n) time using Map, then applies a ranking model that prioritizes the current user, then the post author, then engagement, then recency:

const commentMap = new Map<string, Comment>();
const repliesMap = new Map<string, Comment[]>();
const topLevelComments: Comment[] = [];

comments.forEach((comment) => {
  const commentWithLike: Comment = {
    ...comment,
    user_liked: likedCommentIds.has(comment.id),
    replies: []
  };
  commentMap.set(comment.id, commentWithLike);
Enter fullscreen mode Exit fullscreen mode

That is a better choice than recursive insertion during fetch because it keeps the construction cost predictable as the thread grows. The companion realtime hook, usePostCommentsSubscription(), then merges inserts and deletes back into the cached tree instead of forcing a full reload.

Chat is the other area where the implementation gets interesting. services/chats.service.ts is more imperative than the post service because messaging needs attachment handling, unread counts, and reply references. useSendMessage() in hooks/use-chats.ts creates an optimistic message first, including local attachment previews, then replaces it when the insert completes. useChatSubscription() listens to postgres_changes for the current chat and merges new rows into ["chats", chatId, "messages"]. chat-zone.tsx then groups messages by day and collapses adjacent messages from the same sender in the same minute.

Chat screen

Communities and connections follow the same architectural style with less cache complexity. services/communities.service.ts handles membership joins, image uploads to the community-images storage bucket, search via textSearch("search_vector", filter.search), and role-aware membership state. services/connection.service.ts is careful about ownership, especially in acceptConnectionRequest(), rejectConnectionRequest(), and removeConnection().

API Integration, Performance, Security, and Accessibility

Most integration work happens from the browser through the Supabase client in services/*. That keeps feature code easy to follow, but it also means many services start with supabase.auth.getUser(). The repeated check adds chatter and keeps a lot of data access in the client. The main server-side exception is auth in services/auth.service.ts, plus app/api/link-preview/route.ts, which fetches arbitrary URLs and parses metadata with cheerio.

Performance-wise, the project makes several strong choices. The feed uses useInfiniteQuery() and useInView() for incremental loading in app/(logged-in)/_components/feed.tsx. Comments use linear-time tree construction. Posts, comments, and chat all use optimistic writes. Realtime subscriptions avoid waiting for manual refresh. next.config.mjs also enables PWA behavior through @ducanh2912/next-pwa.

There are tradeoffs. One global QueryClient in components/providers/providers.tsx is simple, but cache policy is mostly default behavior. Chat and community fetches still show some N+1 tendencies, particularly where last messages, participants, unread counts, and reply targets are fetched in follow-up queries. There is also no visible automated test suite, which makes optimistic and realtime paths riskier to change.

On security, the foundations are reasonable. Middleware protects private routes. Auth actions sanitize redirects. Service methods often check ownership before mutation. Still, the design assumes Supabase Row Level Security is doing a lot of the final enforcement. The link preview route deserves extra scrutiny because fetching arbitrary external URLs is exactly where SSRF concerns start.

Accessibility is mixed in the way many product-driven apps are mixed. The baseline is good because the app leans on Radix primitives like components/ui/dialog.tsx, which already carry focus management and semantics. Forms and buttons are generally explicit. But there is no dedicated accessibility layer or test strategy, so quality will depend on each custom component.

What I Would Keep and What I Would Change

The service-plus-hook split is the right core decision here. It keeps Supabase logic out of the view layer, makes React Query reusable, and gives the app room to add richer UX such as optimistic comments and live chat without turning components into query scripts.

What I would change is mostly about tightening boundaries. I would consolidate repeated auth and query helpers, move more high-value writes behind server-side boundaries, add tests around optimistic cache transitions, and review the N+1 paths in chat and community fetches. The current architecture works, but it is at the point where more features will increase coordination cost unless the data access patterns become more deliberate.

Top comments (0)