Configuration

Environment setup, API configuration, and customization options

Environment Variables

Configure your application using environment variables:

.envbash
# .env file for Flutter app
API_BASE_URL=https://api.gridova.com
WS_URL=wss://api.gridova.com/ws
API_TIMEOUT=30000

# Firebase (for push notifications)
FIREBASE_API_KEY=your_firebase_api_key
FIREBASE_PROJECT_ID=gridova-project
FIREBASE_MESSAGING_SENDER_ID=123456789

# App Configuration
APP_NAME=Gridova
APP_VERSION=1.5.2
MIN_ANDROID_VERSION=21
MIN_IOS_VERSION=12.0

# Feature Flags
ENABLE_ANALYTICS=true
ENABLE_CRASH_REPORTING=true
ENABLE_DEBUG_MODE=false

# Payment Gateway (optional)
MIDTRANS_CLIENT_KEY=your_midtrans_client_key
XENDIT_PUBLIC_KEY=your_xendit_public_key

Load Environment in Flutter

main.dartdart
import 'package:flutter_dotenv/flutter_dotenv.dart';

void main() async {
  // Load .env file
  await dotenv.load(fileName: ".env");
  
  runApp(MyApp());
}

// Usage
final apiUrl = dotenv.env['API_BASE_URL'];
final wsUrl = dotenv.env['WS_URL'];

API Configuration

Flutter API Client Setup

api_client.dartdart
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

class ApiClient {
  late Dio _dio;
  String? _authToken;
  
  ApiClient() {
    _dio = Dio(BaseOptions(
      baseUrl: dotenv.env['API_BASE_URL'] ?? '',
      connectTimeout: Duration(milliseconds: 30000),
      receiveTimeout: Duration(milliseconds: 30000),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    ));
    
    // Add interceptors
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        // Add auth token to all requests
        if (_authToken != null) {
          options.headers['Authorization'] = 'Bearer $_authToken';
        }
        return handler.next(options);
      },
      onError: (error, handler) {
        // Handle errors globally
        if (error.response?.statusCode == 401) {
          // Token expired - refresh or logout
          _handleUnauthorized();
        }
        return handler.next(error);
      },
    ));
  }
  
  void setAuthToken(String token) {
    _authToken = token;
  }
  
  void clearAuthToken() {
    _authToken = null;
  }
  
  Future<Response> get(String path, {Map<String, dynamic>? params}) {
    return _dio.get(path, queryParameters: params);
  }
  
  Future<Response> post(String path, {dynamic data}) {
    return _dio.post(path, data: data);
  }
  
  Future<Response> put(String path, {dynamic data}) {
    return _dio.put(path, data: data);
  }
  
  Future<Response> delete(String path) {
    return _dio.delete(path);
  }
  
  void _handleUnauthorized() {
    // Clear token and redirect to login
    clearAuthToken();
    // Navigate to login screen
  }
}

// Usage
final apiClient = ApiClient();
apiClient.setAuthToken('your_jwt_token');

// Make requests
final response = await apiClient.get('/devices');
final devices = response.data['data'];

ESP32 API Configuration

gridova_api.hcpp
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

class GridovaAPI {
  private:
    String baseUrl;
    String authToken;
    WiFiClientSecure client;
    
  public:
    GridovaAPI(String url, String token) {
      baseUrl = url;
      authToken = token;
      client.setInsecure();  // Skip certificate validation (not recommended for production)
    }
    
    bool sendSensorData(String deviceId, float voltage, float current, float power) {
      HTTPClient http;
      
      String url = baseUrl + "/sensor-data";
      http.begin(client, url);
      http.addHeader("Content-Type", "application/json");
      http.addHeader("Authorization", "Bearer " + authToken);
      
      // Create JSON payload
      String payload = "{\"device_id\":\"" + deviceId + "\",";
      payload += "\"voltage\":" + String(voltage, 2) + ",";
      payload += "\"current\":" + String(current, 3) + ",";
      payload += "\"power\":" + String(power, 2) + "}";
      
      int httpCode = http.POST(payload);
      bool success = (httpCode >= 200 && httpCode < 300);
      
      if (!success) {
        Serial.printf("API Error: %d\n", httpCode);
      }
      
      http.end();
      return success;
    }
    
    bool controlRelay(String deviceId, int relay, bool state) {
      HTTPClient http;
      
      String url = baseUrl + "/relay/control";
      http.begin(client, url);
      http.addHeader("Content-Type", "application/json");
      http.addHeader("Authorization", "Bearer " + authToken);
      
      String payload = "{\"device_id\":\"" + deviceId + "\",";
      payload += "\"relay\":" + String(relay) + ",";
      payload += "\"state\":" + (state ? "true" : "false") + "}";
      
      int httpCode = http.POST(payload);
      bool success = (httpCode >= 200 && httpCode < 300);
      
      http.end();
      return success;
    }
};

// Usage
GridovaAPI api(API_URL, AUTH_TOKEN);
api.sendSensorData(DEVICE_ID, 220.5, 2.83, 623.81);

Theme Customization

Customize Gridova colors and styling to match your brand:

Flutter Theme

theme.dartdart
import 'package:flutter/material.dart';

class GridovaTheme {
  // Primary Gridova colors
  static const Color primary = Color(0xFF009688);  // Teal
  static const Color accent = Color(0xFF00796B);   // Dark teal
  static const Color background = Color(0xFFF5F5F5);
  static const Color surface = Color(0xFFFFFFFF);
  static const Color error = Color(0xFFB00020);
  
  static ThemeData lightTheme = ThemeData(
    useMaterial3: true,
    brightness: Brightness.light,
    colorScheme: ColorScheme.light(
      primary: primary,
      secondary: accent,
      surface: surface,
      background: background,
      error: error,
    ),
    appBarTheme: AppBarTheme(
      backgroundColor: primary,
      foregroundColor: Colors.white,
      elevation: 0,
    ),
    cardTheme: CardTheme(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: primary,
        foregroundColor: Colors.white,
        padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    ),
  );
  
  static ThemeData darkTheme = ThemeData(
    useMaterial3: true,
    brightness: Brightness.dark,
    colorScheme: ColorScheme.dark(
      primary: primary,
      secondary: accent,
      surface: Color(0xFF1E1E1E),
      background: Color(0xFF121212),
      error: Color(0xFFCF6679),
    ),
    appBarTheme: AppBarTheme(
      backgroundColor: Color(0xFF1E1E1E),
      foregroundColor: Colors.white,
      elevation: 0,
    ),
  );
}

// Usage in main.dart
MaterialApp(
  theme: GridovaTheme.lightTheme,
  darkTheme: GridovaTheme.darkTheme,
  themeMode: ThemeMode.system,
  // ...
)

Web Theme (Tailwind CSS)

globals.csscss
// globals.css
@import "tailwindcss";

@theme inline {
  /* Primary Gridova Color */
  --color-primary: oklch(62% 0.15 174);
  --color-primary-foreground: oklch(100% 0 0);
  
  /* Accent Colors */
  --color-accent: oklch(55% 0.15 174);
  --color-accent-foreground: oklch(100% 0 0);
  
  /* Background */
  --color-background: oklch(100% 0 0);
  --color-foreground: oklch(9% 0 0);
  
  /* Muted */
  --color-muted: oklch(96% 0 0);
  --color-muted-foreground: oklch(45% 0 0);
  
  /* Card */
  --color-card: oklch(100% 0 0);
  --color-card-foreground: oklch(9% 0 0);
  
  /* Border & Input */
  --color-border: oklch(89% 0 0);
  --color-input: oklch(89% 0 0);
  
  /* Success, Warning, Error */
  --color-success: oklch(65% 0.15 145);
  --color-warning: oklch(75% 0.15 85);
  --color-destructive: oklch(58% 0.22 27);
  
  /* Ring */
  --color-ring: oklch(62% 0.15 174);
  
  /* Radius */
  --radius: 0.5rem;
}

@custom-variant dark (&:is(.dark *));

@theme inline dark {
  --color-background: oklch(13% 0 0);
  --color-foreground: oklch(98% 0 0);
  
  --color-muted: oklch(20% 0 0);
  --color-muted-foreground: oklch(64% 0 0);
  
  --color-card: oklch(13% 0 0);
  --color-card-foreground: oklch(98% 0 0);
  
  --color-border: oklch(27% 0 0);
  --color-input: oklch(27% 0 0);
}

Database Configuration

PostgreSQL Setup

schema.sqlsql
-- Create database
CREATE DATABASE gridova;

-- Create user
CREATE USER gridova_user WITH PASSWORD 'secure_password';

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE gridova TO gridova_user;

-- Connect to database
\c gridova

-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";  -- For text search
CREATE EXTENSION IF NOT EXISTS "timescaledb";  -- For time-series data

-- Create main tables (example)
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email VARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(255) NOT NULL,
  role VARCHAR(50) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE devices (
  id VARCHAR(50) PRIMARY KEY,
  room_id UUID REFERENCES rooms(id),
  name VARCHAR(255) NOT NULL,
  status VARCHAR(20) DEFAULT 'offline',
  last_seen TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Create hypertable for sensor data (TimescaleDB)
CREATE TABLE sensor_data (
  time TIMESTAMPTZ NOT NULL,
  device_id VARCHAR(50) NOT NULL,
  voltage DECIMAL(6,2),
  current DECIMAL(6,3),
  power DECIMAL(8,2),
  energy DECIMAL(10,3),
  frequency DECIMAL(5,2),
  power_factor DECIMAL(4,2)
);

SELECT create_hypertable('sensor_data', 'time');

-- Create indexes
CREATE INDEX idx_sensor_data_device_time 
  ON sensor_data (device_id, time DESC);

CREATE INDEX idx_devices_status 
  ON devices (status);

-- Set up automatic data retention (keep 30 days)
SELECT add_retention_policy('sensor_data', INTERVAL '30 days');

Connection Pool Configuration

database.tstypescript
import { Pool } from 'pg';

const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME || 'gridova',
  user: process.env.DB_USER || 'gridova_user',
  password: process.env.DB_PASSWORD,
  
  // Connection pool settings
  min: 2,                    // Minimum connections
  max: 20,                   // Maximum connections
  idleTimeoutMillis: 30000,  // Close idle connections after 30s
  connectionTimeoutMillis: 2000,  // Timeout if connection takes > 2s
});

pool.on('error', (err) => {
  console.error('Unexpected error on idle client', err);
  process.exit(-1);
});

export default pool;

Security Configuration

HTTPS/TLS Configuration
  • Always use HTTPS in production (TLS 1.2 or higher)
  • Obtain SSL certificate from Let's Encrypt or commercial CA
  • Configure proper cipher suites and disable weak protocols
  • Enable HSTS (HTTP Strict Transport Security)
  • Use WSS (WebSocket Secure) for real-time connections
API Rate Limiting
rate_limiter.tstypescript
import rateLimit from 'express-rate-limit';

// General API rate limiting
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,  // Max 100 requests per window
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', apiLimiter);

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // Max 5 login attempts per 15 minutes
  skipSuccessfulRequests: true,
});

app.use('/api/auth/login', authLimiter);
CORS Configuration
cors_config.tstypescript
import cors from 'cors';

const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://gridova.com',
      'https://app.gridova.com',
      'http://localhost:3000',  // Development only
    ];
    
    if (!origin || allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsOptions));
Input Validation
validation.tstypescript
import { body, validationResult } from 'express-validator';

// Validation middleware
const validateSensorData = [
  body('device_id').isString().trim().notEmpty(),
  body('voltage').isFloat({ min: 0, max: 300 }),
  body('current').isFloat({ min: 0, max: 100 }),
  body('power').isFloat({ min: 0 }),
  
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ 
        success: false,
        errors: errors.array() 
      });
    }
    next();
  }
];

app.post('/api/sensor-data', validateSensorData, async (req, res) => {
  // Process validated data
});

Monitoring & Logging

Winston Logger Setup

logger.tstypescript
import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    // Write to file
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'logs/combined.log' 
    }),
  ],
});

// Console logging in development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

// Usage
logger.info('Server started', { port: 3000 });
logger.error('Database connection failed', { error: err.message });

Health Check Endpoint

health_check.tstypescript
app.get('/health', async (req, res) => {
  const health = {
    uptime: process.uptime(),
    timestamp: Date.now(),
    status: 'ok',
    services: {
      database: 'checking',
      redis: 'checking',
    }
  };
  
  try {
    // Check database
    await pool.query('SELECT 1');
    health.services.database = 'ok';
  } catch (err) {
    health.services.database = 'error';
    health.status = 'degraded';
  }
  
  try {
    // Check Redis
    await redisClient.ping();
    health.services.redis = 'ok';
  } catch (err) {
    health.services.redis = 'error';
    health.status = 'degraded';
  }
  
  const statusCode = health.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(health);
});

Next Steps

Deployment Guide
Learn how to deploy Gridova to production environment
API Documentation
Explore complete REST API and WebSocket endpoints
Hardware Setup
Configure ESP32, PZEM sensor, and relay modules