Back to Projects
Internal System
LLDIKTI V

Eksis — Internal Letter & Disposition Management System

Digital correspondence platform connecting a regional higher-education regulator, universities, and their governing foundations, replacing paper-based letters and disposition with a real-time digital workflow.

React
TypeScript
Hono.js
Drizzle ORM
PostgreSQL
Redis
WebSocket

Overview

Eksis is a digital correspondence platform built during an MBKM Fullstack Internship at LLDIKTI V Yogyakarta, the regional regulator overseeing private higher-education institutions. It connects three types of organizations — the regulator (LLDIKTI), universities (PT), and the foundations that run them (BP) — on one platform for sending letters, routing disposition (internal follow-up instructions), and tracking status, with a hybrid role- and attribute-based access model (RBAC + ABAC) so every organization type only sees what's relevant to it.

Dashboard


Screen Preview & Interface

| Login | Dashboard | | :---: | :---: | | Login Page | Dashboard | | Session-based auth with account lockout after repeated failed attempts | Overview of letters, dispositions, and activity relevant to the logged-in organization |

| Send Letter | Profile | | :---: | :---: | | Send Letter | Profile | | Compose & send flow with auto-generated tracking number and multi-file attachment | User profile, tied to the organization-specific record (LLDIKTI/PT/BP) |


Problem

  • Official correspondence between the regulator, universities, and foundations was paper-based and slow to track.
  • Internal follow-up on incoming letters (disposition) had no digital trail, making it hard to know who was responsible for what, and by when.
  • Staff and partner institutions had no way to check a letter's status without contacting the office directly.

Solution

  • Digital letter workflow: send, receive, and track official letters between institutions, each with an auto-generated tracking number.
  • Disposition & follow-up: route incoming letters to the right person with instructions and a deadline, and track responses through to completion.
  • Real-time notifications (in-app, push, and email) so nothing sits unnoticed in an inbox.
  • Public tracking: partner institutions can check a letter's status without needing an account, similar to tracking a package.
  • Role-based access tailored to each type of organization, so every user only sees what's relevant to them.

Architecture

Layered/clean architecture on the backend (domain → application → infra → presentation), paired with a React SPA/PWA frontend. Every feature (letters, dispositions, auth, users, references, …) has its own use-case class injected via a DI container, called from HTTP controllers through a shared use-case interface.

flowchart LR
    subgraph Client
        FE[React SPA / PWA]
    end
    subgraph Backend["Backend (Hono, layered architecture)"]
        R[presentation<br/>routes + controllers]
        UC[application<br/>use-cases]
        REPO[infra<br/>repositories, Drizzle ORM]
        MW[middleware<br/>JWT · permission · upload]
    end
    DB[(PostgreSQL)]
    REDIS[(Redis)]
    Q[Queue<br/>email · notification]
    WS[WebSocket]
    PUSH[Web Push]

    FE -->|REST/JSON, multipart| MW --> R --> UC --> REPO --> DB
    UC -->|enqueue job| Q --> REDIS
    Q --> PUSH -->|browser push| FE
    UC -->|emit event| WS -->|realtime| FE
    FE -.->|WS auth via JWT| WS

Login Flow

sequenceDiagram
    participant U as User
    participant FE as Frontend (React)
    participant BE as Backend (Hono)
    participant DB as PostgreSQL

    U->>FE: enter username + password
    FE->>BE: POST /auth/login
    BE->>DB: look up user
    alt user not found / wrong password
        BE->>DB: increment failed_login_attempts
        Note over BE: 5x failures → locked for 10 minutes
        BE-->>FE: 400/403 error
    else valid
        BE->>DB: load org profile (LLDIKTI/PT/BP) + roles
        Note over BE: incomplete organization profile → blocked with a specific message
        BE->>BE: issue JWT access + refresh token<br/>(claims: userId, roles, org id)
        BE->>DB: store token, update last_login
        BE-->>FE: access_token, refresh_token
    end
    FE->>FE: store token (client-side state)
    FE->>BE: GET /auth/permissions (build menu from access)

Send Letter → Disposition → Response

flowchart TD
    A[User sends a letter] -->|permission: send.letter| B{Upload files<br/>main letter + attachments}
    B --> C[Auto-generate tracking number<br/>per year + letter type]
    C --> D[Save letter + recipients<br/>polymorphic: user / LLDIKTI dept / BP / PT / external]
    D --> E[Initial status recorded]
    E --> F[Notify recipients<br/>in-app + push + WebSocket]
    F --> G{Recipient opens letter}
    G -->|optional| H[Forward letter]
    G --> I[Create disposition<br/>permission: create.disposition]
    I --> J[Disposition recipients<br/>+ actions flagged]
    J --> K[Notify disposition recipients]
    K --> L[Recipient responds<br/>+ file attachment]
    L --> M[Status history recorded]
    M --> N[Disposition marked complete]

Supporting features: antedated letters (letters whose real creation date differs from the system input date, recorded with a reason), public tracking for external contacts to check a letter's status without logging in, a full history/relations timeline per letter (replies, parent/child), and two-phase file uploads (temporary → permanent) to avoid orphaned files from failed submissions.

Notifications (multi-channel)

flowchart LR
    EV[Use-case event<br/>new letter / new disposition / etc.] --> Q[Queue<br/>Redis]
    Q --> N1[Notification job]
    Q --> N2[Email job]
    N1 --> DB[(notifications table)]
    N1 --> WS[WebSocket push<br/>to online clients]
    N1 --> PUSH[Web Push<br/>to browser subscribers]
    N2 --> MAIL[Send email]

Data Model (ERD)

Letters & Disposition

letters and letter_recipients use a polymorphic sender/recipient: exactly one of the user / department(LLDIKTI) / foundation(BP) / university(PT) / external-contact references is set, depending on recipient type.

erDiagram
    USERS ||--o{ LETTERS : "created_by"
    USERS ||--o{ LETTER_RECIPIENTS : "recipient"
    USERS ||--o{ DISPOSITIONS : "from_user"
    USERS ||--o{ DISPOSITION_RECIPIENTS : "to_user"

    LETTERS ||--o{ LETTER_RECIPIENTS : has
    LETTERS ||--o{ LETTER_FILES : has
    LETTERS ||--o{ LETTER_TAGS : has
    LETTERS ||--o{ DISPOSITIONS : triggers
    LETTERS ||--o| ANTEDATED_LETTERS : "if antedated"
    LETTERS ||--o{ LETTER_PROGRESS : "tracked by"
    LETTERS }o--o| LETTERS : "parent_letter (reply/relation)"

    LETTER_FILES }o--|| FILES : references
    LETTER_RECIPIENTS }o--o| EXTERNAL_CONTACTS : "if external"

    DISPOSITIONS ||--o{ DISPOSITION_RECIPIENTS : "sent to"
    DISPOSITIONS ||--o{ DISPOSITION_ACTIONS : flags
    DISPOSITIONS ||--o{ DISPOSITION_RESPONSES : responses
    DISPOSITION_RESPONSES ||--o{ DISPOSITION_RESPONSE_FILES : attachments

    LETTERS {
        uuid letter_id PK
        varchar letter_number UK
        varchar agenda_number UK
        date date
        varchar subject
        uuid created_by_id FK
        int status_id FK
        int confidentiality_id FK
        int priority_id FK
        boolean is_antedated_letter
        uuid parent_letter_id FK
    }
    DISPOSITIONS {
        uuid disposition_id PK
        uuid letter_id FK
        uuid from_user_id FK
        text instruction
        int status_id FK
        boolean is_public
        date deadline_date
        boolean is_need_report
    }
    FILES {
        uuid file_id PK
        varchar file_name
        text file_url
        enum file_status "temporary|permanent"
        boolean is_external_file
    }

User, Role & Permission (hybrid RBAC + ABAC)

Two-tier permission model: role → permission by default, with per-user overrides via a direct user-permission table. Each permission can be scoped more finely by attribute (e.g. a "view letter" permission restricted to a specific data scope).

erDiagram
    REF_USER_TYPE ||--o{ USERS : categorizes
    REF_USER_TYPE ||--o{ REF_ROLES : "role scoped per type"

    USERS ||--o| BP_USERS : "profile if BP"
    USERS ||--o| PT_USERS : "profile if PT"
    USERS ||--o| LLDIKTI_USERS : "profile if LLDIKTI"

    USERS ||--o{ USER_ROLES : has
    USERS ||--o{ USER_PERMISSIONS : "direct override"
    REF_ROLES ||--o{ USER_ROLES : "assigned to"
    REF_ROLES ||--o{ ROLE_PERMISSIONS : grants

    REF_PERMISSIONS ||--o{ ROLE_PERMISSIONS : via
    REF_PERMISSIONS ||--o{ USER_PERMISSIONS : via
    REF_PERMISSION_ATTRIBUTES ||--o{ ROLE_PERMISSIONS : scope
    REF_PERMISSION_ATTRIBUTES ||--o{ USER_PERMISSIONS : scope

    USERS {
        uuid user_id PK
        varchar username UK
        varchar email UK
        int user_type_id FK
        boolean is_active
        int failed_login_attempts
        timestamp blocked_until
    }
    REF_ROLES {
        int role_id PK
        varchar role_name
        int user_type_id FK
    }
    REF_PERMISSIONS {
        uuid permission_id PK
        varchar permission_name
    }

Permissions follow an action.resource naming convention (e.g. send.letter, view.letter, create.disposition, update.forward-letter), enforced per-route by a permission middleware, with JWT claims carrying roles and organization id so authorization checks don't need extra queries.


Impact

  • Replaced manual, paper-based correspondence with a trackable digital workflow across the regulator, universities, and foundations.
  • Reduced the time needed to follow up on and resolve internal dispositions.
  • Gave every institution visibility into the real-time status of their own correspondence.
Screenshots
Eksis — Internal Letter & Disposition Management System — screenshot 1
Eksis — Internal Letter & Disposition Management System — screenshot 2
Eksis — Internal Letter & Disposition Management System — screenshot 3
Eksis — Internal Letter & Disposition Management System — screenshot 4
Eksis — Internal Letter & Disposition Management System — screenshot 5