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.
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.

Screen Preview & Interface
| Login | 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 |
| :---: | :---: |
|
|
|
| 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
lettersandletter_recipientsuse 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.




