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_keyLoad 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
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);
});