Tay Za
Back to Garden

SecureXchange

2026-06-04
projectdocumentation

SecureXchange: Technical Architecture & System Documentation

SecureXchange is a premium-tier, real-time, bi-directional currency conversion and transaction processing web application designed specifically for the Thai Baht (THB) ↔ Myanmar Kyat (MMK) exchange pair. The platform integrates a high-end dark obsidian visual design with strict financial precision, real-time reactive data streaming, role-based access controls (RBAC), and deep mobile integration via the LINE Messaging API and LINE LIFF SDK.


1. System & Architecture Overview

SecureXchange is engineered on a serverless, decoupled modern stack that ensures immediate state synchronization, tight security boundaries, and frictionless user experiences inside the LINE application ecosystem.

graph TD
    subgraph Client [Client-Side Layer (React 18 / Vite / TS)]
        A[LINE LIFF Browser / Web Client] -->|Fetch Identity / Profile| B(useLiffIdentity Hook)
        A -->|Subscribe Exchange Rates| C(useExchangeRates Hook)
        A -->|Submit Swap Requests| D(ExchangeForm Component)
        A -->|Access Control Gating| E(Admin Portal & Queue)
    end

    subgraph Serverless [Serverless & Real-time Layer]
        F[(Supabase Postgres Database)]
        G((Supabase Realtime Engine))
        H[Deno Edge Function: send-line-exchange]
    end

    subgraph Messaging [LINE Messaging Ecosystem]
        I[LINE Messaging API Gateway]
        J[Merchant Line Account]
        K[Customer Line Chat]
    end

    C <==>|Realtime Subscription / TLS Websocket| G
    G <==>|CDC Change Data Capture| F
    D -->|REST / CORS API POST Payload| H
    H -->|Validate Rate Limit / Persist SQL| F
    H -->|Secure RPC Push Notification| I
    I -->|Direct Message Alert| J
    I -->|Receipt Auto-Reply| K
    E -->|Write Rates / Update Status| F

Architecture Key Principles:

  1. Reactive Rate Updates: Eliminates polling overhead. Exchange rates are bound to a PostgreSQL CDC (Change Data Capture) stream over WebSockets via Supabase Realtime.
  2. LINE-First Execution: The application detects if it is running inside the LINE LIFF custom tab context, automatically authenticates user identity, and feeds metadata (LINE display name, unique ID) down to transactional pipelines.
  3. RBAC at Database Layer: Role validation is enforced at both frontend routes and PostgreSQL Row-Level Security (RLS) policies using customized security handler functions.
  4. Synchronous Serverless Edge Processing: Rather than relying on heavy persistent workers or async job queues, transactional exchanges and merchant push operations are processed synchronously inside lightweight Deno V8 isolates (Supabase Edge Functions).

2. Database Schema Design (PostgreSQL)

The database schema is highly optimized for security, rate synchronization, and auditability. It leverages built-in Postgres triggers for automatic role provisioning and strict RLS mechanisms.

2.1 Table: exchange_rates

Manages the current bid/ask parameters for the currency pair. This table has exactly one active row which is updated by administrators and listened to by all active clients.

| Column | Type | Constraints | Default / Description | | :--- | :--- | :--- | :--- | | id | bigint | PRIMARY KEY, GENERATED BY DEFAULT AS IDENTITY | Auto-incrementing identifier. | | buying_rate | numeric | NOT NULL | High-precision multiplier for THB → MMK calculations. | | selling_rate | numeric | NOT NULL | High-precision divisor for MMK → THB calculations. | | updated_at | timestamp with time zone | NOT NULL | timezone('utc'::text, now()) - Tracks rate freshness. |

RLS Policies on exchange_rates:

  • Select Policy (Enable read access for all users): Open to public/authenticated users.
  • Write Policy (Enable insert/update/delete for admins only): Restricted to users who pass the customized role validation helper function:
    (auth.uid() IN ( SELECT user_roles.user_id
                     FROM user_roles
                     WHERE (user_roles.role = 'admin'::text)))
    

2.2 Table: transactions

Records user-submitted trade requests, tracking statuses and associated LINE identifiers for automated notifications.

| Column | Type | Constraints | Default / Description | | :--- | :--- | :--- | :--- | | id | uuid | PRIMARY KEY | gen_random_uuid() - Cryptographically secure unique key. | | user_id | uuid | FOREIGN KEY references auth.users(id) | Identifies the authenticated creator. | | from_currency | text | NOT NULL | The source currency (e.g., 'THB' or 'MMK'). | | to_currency | text | NOT NULL | The destination currency (e.g., 'MMK' or 'THB'). | | amount | numeric | NOT NULL | Swap volume up to 4 decimal places. | | rate | numeric | NOT NULL | The exchange rate locked in at submission time. | | status | text | CHECK (status IN ('pending', 'approved', 'completed', 'rejected')) | Defaults to 'pending'. | | line_user_id | text | NULLABLE | Customer's identifier parsed from LINE LIFF context. | | line_display_name | text | NULLABLE | Customer's display name parsed from LINE LIFF context. | | idempotency_key | text | UNIQUE | Prevents double-charging during concurrent requests. | | created_at | timestamp with time zone | NOT NULL | timezone('utc'::text, now()) |

RLS Policies on transactions:

  • Admin Access (Admins can do everything): Granted full select, insert, update, and delete access based on the 'admin' user role.
  • User Access (Users can view their own transactions): Selective filtering using auth.uid() = user_id.
  • User Insertion (Users can insert their own transactions): Restricted to insertion where the inserted record's user_id matches auth.uid().

2.3 Table: user_roles

Coordinates the granular role allocation (RBAC) supporting the SecureXchange security layer.

| Column | Type | Constraints | Default / Description | | :--- | :--- | :--- | :--- | | id | uuid | PRIMARY KEY | gen_random_uuid() | | user_id | uuid | FOREIGN KEY references auth.users(id), UNIQUE | One role per user. | | role | text | CHECK (role IN ('admin', 'user')) | Defaults to 'user'. | | created_at | timestamp with time zone | NOT NULL | timezone('utc'::text, now()) |

Custom Database Functions & Database Triggers:

  1. Trigger Function: handle_new_user: Automatically executes upon new user registration inside auth.users. It dynamically awards administrative rights to the very first user registering on the platform, while all subsequent registrations default to 'user'.
    CREATE OR REPLACE FUNCTION public.handle_new_user()
    RETURNS trigger
    LANGUAGE plpgsql
    SECURITY DEFINER AS $$
    DECLARE
      is_first BOOLEAN;
    BEGIN
      -- Check if any user already exists in user_roles
      SELECT NOT EXISTS (SELECT 1 FROM public.user_roles) INTO is_first;
      
      IF is_first THEN
        INSERT INTO public.user_roles (user_id, role)
        VALUES (new.id, 'admin');
      ELSE
        INSERT INTO public.user_roles (user_id, role)
        VALUES (new.id, 'user');
      END IF;
      
      RETURN new;
    END;
    $$;
    
  2. Helper RPC: has_role: A security helper exposed via POST RPC endpoints to quickly verify authorization context in SQL and UI:
    CREATE OR REPLACE FUNCTION public.has_role(role_to_check text)
    RETURNS boolean
    LANGUAGE plpgsql
    SECURITY DEFINER AS $$
    BEGIN
      RETURN EXISTS (
        SELECT 1 FROM public.user_roles
        WHERE user_id = auth.uid() AND role = role_to_check
      );
    END;
    $$;
    

3. Backend Edge Functions (Deno Runtime)

The platform backend resides in a highly focused Deno edge worker, /supabase/functions/send-line-exchange/index.ts, processing transaction requests and interacting directly with the LINE Messaging API.

3.1 Flow & Execution Diagram

sequenceDiagram
    autonumber
    actor Client as React Client
    participant EF as Edge Function (send-line-exchange)
    participant DB as Postgres Database
    participant LINE as LINE Messaging API
    
    Client->>EF: POST transaction payload + User Auth Bearer JWT
    Note over EF: Validates CORS headers<br/>Parses authorization bearer token
    
    EF->>DB: Check last transaction timestamp (Rate Limit verification)
    DB-->>EF: Return last transaction timestamp
    
    alt Time elapsed < 60 seconds
        EF-->>Client: HTTP 429 [Too Many Requests] (Rate Limit Triggered)
    else Rate Limit Cleared
        EF->>DB: Query current exchange_rates table
        DB-->>EF: Return active rates
        Note over EF: Recalculates amount & rate in sandbox<br/>Ensures no tampering by the client
        
        EF->>DB: Insert new transaction with 'pending' status
        DB-->>EF: Transaction persisted & UUID returned
        
        EF->>LINE: Send Push Notification (To Merchant Account)
        LINE-->>EF: Status 200 OK
        
        alt User submitted from LINE LIFF context
            EF->>LINE: Send Confirmation Push (To User line_user_id)
            LINE-->>EF: Status 200 OK
        end
        
        EF-->>Client: HTTP 200 OK + Transaction Object
    end

3.2 Secure Rate Limiting and Verification Logic

To maintain the financial integrity of the system and prevent denial of service (DoS):

  1. Rate Limiting Constraint: An active user (user_id) or a distinct LINE identity (line_user_id) is strictly restricted to one transaction per minute (60 seconds). This prevents double-click race conditions and transaction flooding.
  2. Server-Side Rate Recalculation: The Edge Function never trusts the calculated total or conversion rate passed from the client-side UI. Instead, the function queries the exchange_rates database table directly at the moment of receipt and reconstructs all conversion metrics using secure server-side arithmetic.
  3. CORS Guard: Strict Pre-flight OPTIONS handling ensures external unauthorized client environments are immediately blocked at the edge.

3.3 Deno Environment Variables (Secrets)

The Edge Function requires the following variables configured via supabase secrets set:

  • LINE_CHANNEL_ACCESS_TOKEN: Authorization bearer token configured within the LINE Developers Console.
  • MERCHANT_LINE_ID: The unique user ID of the merchant receiving transactional alerts.
  • SUPABASE_URL: Local or production backend URL.
  • SUPABASE_SERVICE_ROLE_KEY: Service bypass key used to authenticate system-level database insertions.

4. Frontend Client Architecture

The frontend is developed using React 18, compiled using Vite, and structured to allow bi-directional switching alongside instant rate updates.

src/
├── components/
│   ├── admin/
│   │   ├── MarginControls.tsx       # Set exchange rate inputs
│   │   └── TransactionQueue.tsx     # Queue state list & status updates
│   ├── exchange/
│   │   ├── ExchangeForm.tsx         # Converter layout & submit trigger
│   │   ├── IndexHeader.tsx          # Navigation, status, language toggle
│   │   └── TrustBadges.tsx          # UX security signals
│   ├── ui/                          # Radix & shadcn primitive definitions
│   ├── BottomNav.tsx                # Context-aware bottom bar
│   ├── CurrencyInput.tsx            # Form-validated numeric input
│   ├── ExchangeRateDisplay.tsx      # Real-time rates with gold glow animation
│   ├── LanguageToggle.tsx           # Dual-lingual translation selection
│   ├── ThemeToggle.tsx              # Context theme manager (Obsidian force-dark)
│   └── theme-provider.tsx           # React Context Theme Provider
├── config/
│   └── liff.ts                      # Configuration mapping the LINE LIFF App ID
├── hooks/
│   ├── useExchangeRates.ts          # Core TanStack hook with Postgres stream listener
│   └── useLiffIdentity.ts           # LIFF profile initialization and state hook
├── i18n/
│   ├── locales/
│   │   ├── en.json                  # English translations
│   │   └── my.json                  # Myanmar translations (Burmese)
│   └── config.ts                    # react-i18next setup engine
├── integrations/
│   └── supabase/
│       ├── client.ts                # supabase-js initialization
│       └── types.ts                 # Database auto-generated TypeScript schema
├── lib/
│   └── utils.ts                     # Tailwind dynamic style merging tool (cn)
├── pages/
│   ├── Admin.tsx                    # Locked administrator console
│   ├── Auth.tsx                     # Supabase Email & Password / OTP gateway
│   ├── Faq.tsx                      # Local rules, requirements & FAQ portal
│   ├── Guide.tsx                    # Step-by-step conversion walk-through
│   ├── History.tsx                  # Authenticated transaction tracking panel
│   ├── Index.tsx                    # Main transactional converter landing
│   └── NotFound.tsx                 # Fallback Page
├── App.css
├── App.tsx                          # App container wrapping providers
├── index.css                        # Premium custom HSL colors and animation keyframes
└── main.tsx                         # Client DOM rendering target

4.1 Real-time State Synchronization: useExchangeRates

This custom hook utilizes TanStack Query to fetch rates with an infinite stale duration, coupled with an active Supabase real-time database channel that listens to specific target alterations:

// Query cache initialization
const { data: initialRates } = useQuery({
  queryKey: ["exchange_rates"],
  queryFn: async () => {
    const { data, error } = await supabase
      .from("exchange_rates")
      .select("*")
      .single();
    if (error) throw error;
    return data;
  },
  staleTime: Infinity, // Avoid polling overhead
});

// Postgres CDC Stream Subscription
useEffect(() => {
  const channel = supabase
    .channel("exchange-rates-changes")
    .on(
      "postgres_changes",
      { event: "UPDATE", schema: "public", table: "exchange_rates" },
      (payload) => {
        // Automatically inject new stream value into TanStack cache
        queryClient.setQueryData(["exchange_rates"], payload.new);
      }
    )
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}, [queryClient]);

4.2 LINE Identity Hook: useLiffIdentity

Initializes the LINE LIFF environment. When the application loads inside a LINE environment, it extracts metadata without forcing redirects, caching authentication state seamlessly:

import liff from "@line/liff";

export const useLiffIdentity = () => {
  const [profile, setProfile] = useState<{ userId: string; displayName: string; pictureUrl?: string } | null>(null);
  const [isLiff, setIsLiff] = useState(false);

  useEffect(() => {
    liff.init({ liffId: "2008550373-7J1o2qld" })
      .then(() => {
        setIsLiff(liff.isInClient());
        if (liff.isLoggedIn()) {
          liff.getProfile().then((p) => setProfile(p));
        } else if (liff.isInClient()) {
          liff.login();
        }
      })
      .catch((err) => console.error("LIFF initialization failed", err));
  }, []);

  return { profile, isLiff };
};

5. UI/UX Style Token Guidelines

SecureXchange implements an customized premium dark-obsidian aesthetic. Standard Tailwind configurations are overlaid with custom smooth glassmorphism structures and visual cues.

5.1 Design System Color Palette (HSL Specs)

Custom HSL tokens are embedded directly within index.css:

:root {
  --background: 240 10% 3.9%;
  --foreground: 0 0% 98%;
  --card: 240 10% 3.9%;
  --card-foreground: 0 0% 98%;
  --popover: 240 10% 3.9%;
  --popover-foreground: 0 0% 98%;
  --primary: 43 100% 49%;        /* #F5A623 - Gold */
  --primary-foreground: 240 5.9% 10%;
  --secondary: 240 3.7% 15.9%;
  --secondary-foreground: 0 0% 98%;
  --muted: 240 3.7% 15.9%;
  --muted-foreground: 240 5% 64.9%;
  --accent: 240 3.7% 15.9%;
  --accent-foreground: 0 0% 98%;
  --destructive: 0 62.8% 30.6%;
  --destructive-foreground: 0 0% 98%;
  --border: 240 3.7% 15.9%;
  --input: 240 3.7% 15.9%;
  --ring: 43 100% 49%;           /* Active gold halo highlights */
}

5.2 Key Premium Visual Utilities (Vanilla CSS Styles)

  • Obsidian Dark Backdrop (obsidian-gradient): A multi-stop dynamic radial-gradient centered around an deep rich onyx backdrop: radial-gradient(circle at top, #141416 0%, #08080a 100%)
  • Glass Panel overlay (glass-panel): Provides clean glass layering using backdrop-filter: blur(16px) and subtle transparency borders to distinguish overlay components.
  • Gold Pulse Indicator (gold-glow): Highlights live status changes using smooth gold CSS box-shadow expansion animations.
  • Tactile Feedback micro-animations: Vibrant CSS transitions applied on interaction targets to deliver physical tactile reactions during fast swaps.

6. Internationalization (i18n)

The dual-lingual engine ensures native readability for both English and Myanmar speakers. Key phrases, operational messages, warnings, and transaction logs are mapped across translation indices.

Language Configurations:

  • Default Locale: 'my' (Myanmar Burmese) is the default selected locale, with seamless toggle switching to 'en' (English).
  • Fallback engine: Automatically handles user preference caching inside local cookies/storage and falls back to 'my' to avoid empty string errors in runtime environments.
  • Translation resources structure:
    • src/i18n/locales/en.json: Houses English localized labels.
    • src/i18n/locales/my.json: Houses highly optimized Myanmar Burmese localizations.

7. Operational Workflows & Life Cycles

7.1 Real-time Conversion Workflow (Bi-directional)

The conversion relies on simple, mathematically sound rules to avoid floating-point drift:

THB to MMK: 
    Output_MMK = Input_THB * Buying_Rate

MMK to THB: 
    Output_THB = Input_MMK / Selling_Rate
  • Calculations are restricted to exactly 4 decimal places when rendering final results.
  • Clicking the Swap Currency Button swaps the from and to values, dynamically updating conversion inputs, calculation indicators, and placeholder values instantly.

7.2 Transaction Lifecycle Workflow

The application handles transactions through a strict workflow with role-based checks.

stateDiagram-v2
    [*] --> Pending : User Submits via Edge Function
    Pending --> Approved : Admin Clicks 'Approve' (Verifies Funds)
    Pending --> Rejected : Admin Clicks 'Reject' (Invalid/No payment)
    Approved --> Completed : Admin Confirms Final Settlement
    Rejected --> [*]
    Completed --> [*]
  1. Pending State: Submitted swaps are immediately visible in the Admin Transaction queue. Rates are locked in upon submission.
  2. Verification: Merchant receives an instant LINE push notification alert containing user details, rates, and cash-in metrics.
  3. Settlement Execution: Once cash transfer is validated, the administrator marks the order as Approved or completes it to close the lifecycle.

8. Observability & Testing

8.1 Error Tracking and Analytics

  • Sentry: Integrated for comprehensive error tracking, performance monitoring, and tracing unhandled exceptions in the React frontend.
  • PostHog: Configured for capturing user product usage telemetry, tracking onboarding progress via the react-joyride integration, and general behavior analytics.

8.2 Automated Testing

  • Vitest: The platform leverages Vitest for executing fast, lightweight unit tests to ensure stability across React components and core functional logic.

9. Deployment & Environmental Configuration

9.1 Frontend Deployment (Vite + Vercel)

  1. Verify .env.local contains valid production variables:
    VITE_SUPABASE_URL=https://[YOUR_SUPABASE_PROJECT].supabase.co
    VITE_SUPABASE_PUBLISHABLE_KEY=[YOUR_ANON_KEY]
    
  2. Compile build package (optimized with manual chunk splitting for faster load times):
    npm run build
    
  3. Deploy static folder contents to Vercel or your hosting environment.

9.2 Backend Deployment (Supabase Edge Function)

Deploy the edge workers and set environment variables:

# Link project to local Supabase configuration
supabase link --project-ref [YOUR-PROJECT-ID]

# Deploy the send-line-exchange Deno serverless function
supabase functions deploy send-line-exchange

# Set mandatory production secrets for LINE integration
supabase secrets set LINE_CHANNEL_ACCESS_TOKEN="your-channel-token"
supabase secrets set MERCHANT_LINE_ID="merchant-line-id"

10. Verification & Troubleshooting

Local Function Testing

To verify Deno serverless worker performance without deploying live, invoke local hosting:

supabase functions serve send-line-exchange --no-verify-jwt

Submit sample payloads containing authorization headers to verify CORS handlers, rate recalculation triggers, and correct PostgreSQL interactions locally.