Skip to main content

Overview

This guide covers installation and configuration for both development and production environments. ICL Cotizaciones is a Next.js 16 application using SQLite as its database, making it straightforward to deploy as a single-server application.
For a quick local setup, see the Quickstart guide. This guide covers production-ready deployment.

System Requirements

Hardware

  • CPU: 2+ cores recommended
  • RAM: 2GB minimum, 4GB recommended
  • Storage: 10GB minimum (database grows with quotation volume)
  • Network: Static IP or domain name for production

Software

  • Node.js: 18.0 or higher (20.x LTS recommended)
  • npm: 9.0 or higher
  • Build Tools: Required for native module compilation
    • macOS: Xcode Command Line Tools
    • Linux: build-essential, python3
    • Windows: Visual Studio Build Tools
1

Clone Repository

Get the source code:
git clone <repository-url> iclapp
cd iclapp
2

Install Dependencies

Install all required npm packages:
npm install
Key Dependencies:
{
  "dependencies": {
    "next": "^16.1.6",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "drizzle-orm": "^0.45.1",
    "better-sqlite3": "^12.6.2",
    "iron-session": "^8.0.4",
    "bcryptjs": "^3.0.3"
  }
}
better-sqlite3 is a native Node.js addon that requires compilation during install. Ensure build tools are available.
3

Configure Environment Variables

Create a .env.local file in the project root:
.env.local
# Session Configuration
SESSION_SECRET="your-secret-key-at-least-32-characters-long-change-this-in-production"
SESSION_SECURE=false  # Set to true in production with HTTPS

# Database Configuration
DATABASE_PATH="./data/icl.db"

# Server Configuration
PORT=3000
NODE_ENV=development
Critical Security Settings:The default session secret in src/lib/session.ts is:
password: "icl-secret-key-at-least-32-characters-long!!"
This must be changed in production via the SESSION_SECRET environment variable. Generate a secure key:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Environment Variable Reference:
VariableDescriptionDefaultRequired
SESSION_SECRETIron-session encryption keyHardcodedYes (production)
SESSION_SECUREEnable HTTPS-only cookiesfalseYes (production)
DATABASE_PATHSQLite database file location./data/icl.dbNo
PORTHTTP server port3000No
NODE_ENVEnvironment (development/production)developmentNo
4

Initialize Database

Create the database directory and initialize schema:
# Create data directory
mkdir -p data

# Generate Drizzle schema
npm run db:generate

# Run migrations (creates tables)
npm run db:migrate

# Seed with sample data (optional for development)
npm run db:seed
Database Schema:The schema is defined in src/db/schema.ts:
export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  full_name: text("full_name").notNull(),
  email: text("email").notNull().unique(),
  password: text("password").notNull(),
  role: text("role", { 
    enum: ["DIRECTOR", "GERENTE", "COMERCIAL", 
           "CSV", "OPERACIONES", "ADMINISTRACION"] 
  }).notNull(),
  is_active: integer("is_active", { mode: "boolean" })
    .notNull().default(true),
  dni: text("dni"),
  created_at: text("created_at").notNull()
    .default("(datetime('now'))"),
  updated_at: text("updated_at").notNull()
    .default("(datetime('now'))"),
});
SQLite Configuration:The database connection in src/db/index.ts uses:
import Database from "better-sqlite3";

const dbPath = path.join(process.cwd(), "data", "icl.db");
const sqlite = new Database(dbPath);

// Enable WAL mode for concurrent reads
sqlite.pragma("journal_mode = WAL");

// Enable foreign key constraints
sqlite.pragma("foreign_keys = ON");
WAL (Write-Ahead Logging) mode allows multiple concurrent readers while a write is in progress, improving performance.
5

Create Initial Admin User

If you didn’t run db:seed, create an admin user manually:
// Create a script: scripts/create-admin.ts
import { db } from "@/db";
import { users } from "@/db/schema";
import { hashSync } from "bcryptjs";

const hashedPassword = hashSync("your-secure-password", 10);

db.insert(users).values({
  full_name: "System Administrator",
  email: "[email protected]",
  password: hashedPassword,
  role: "DIRECTOR",
  is_active: true,
});

console.log("Admin user created");
Run it:
tsx scripts/create-admin.ts
Store admin credentials securely. Never commit passwords to version control.
6

Development Server

Start the development server:
npm run dev
The application runs on http://localhost:3000 with:
  • Hot module replacement
  • Turbopack for fast builds
  • Detailed error overlay
Available Scripts:
package.json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:seed": "tsx src/db/seed.ts"
  }
}

Production Deployment

Build for Production

1

Build Application

Create an optimized production build:
npm run build
This generates:
  • Optimized JavaScript bundles
  • Server-side rendering configuration
  • Static assets
  • Production API routes
Output:
Route (app)                              Size     First Load JS
┌ ○ /                                    142 B          87.9 kB
├ ○ /login                               142 B          87.9 kB
â”” Æ’ /cotizaciones                        5.2 kB         93.1 kB
2

Start Production Server

Run the production server:
npm run start
Or with custom port:
PORT=8080 npm run start

Process Management

# Install PM2 globally
npm install -g pm2

# Start application
pm2 start npm --name "iclapp" -- start

# Enable startup on boot
pm2 startup
pm2 save

# Monitor
pm2 logs iclapp
pm2 monit
PM2 Ecosystem File:
ecosystem.config.js
module.exports = {
  apps: [{
    name: 'iclapp',
    script: 'npm',
    args: 'start',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      SESSION_SECRET: process.env.SESSION_SECRET,
      SESSION_SECURE: 'true',
    },
    instances: 1,
    exec_mode: 'fork',
    max_memory_restart: '1G',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
  }]
};
Start with ecosystem file:
pm2 start ecosystem.config.js

Using systemd

/etc/systemd/system/iclapp.service
[Unit]
Description=ICL Cotizaciones
After=network.target

[Service]
Type=simple
User=iclapp
WorkingDirectory=/opt/iclapp
Environment="NODE_ENV=production"
Environment="PORT=3000"
Environment="SESSION_SECRET=your-secret-key"
Environment="SESSION_SECURE=true"
ExecStart=/usr/bin/npm start
Restart=on-failure

[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable iclapp
sudo systemctl start iclapp
sudo systemctl status iclapp

Reverse Proxy Configuration

nginx

/etc/nginx/sites-available/iclapp
server {
    listen 80;
    listen [::]:80;
    server_name cotizaciones.yourcompany.com;

    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name cotizaciones.yourcompany.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/yourcompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourcompany.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Proxy to Next.js
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    # Client body size limit (for file uploads)
    client_max_body_size 10M;
}
Enable site:
sudo ln -s /etc/nginx/sites-available/iclapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Caddy

Caddyfile
cotizaciones.yourcompany.com {
    reverse_proxy localhost:3000
    encode gzip
    
    # Automatic HTTPS with Let's Encrypt
    tls [email protected]
}
Start Caddy:
caddy run --config Caddyfile

Database Backup Strategy

SQLite databases can become corrupted if not backed up properly. Always use WAL mode and proper backup procedures.
Automated Backup Script:
scripts/backup-db.sh
#!/bin/bash
set -e

# Configuration
DB_PATH="./data/icl.db"
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/icl_${TIMESTAMP}.db"

# Create backup directory
mkdir -p "${BACKUP_DIR}"

# SQLite online backup (safe while app is running)
sqlite3 "${DB_PATH}" ".backup '${BACKUP_FILE}'"

# Compress
gzip "${BACKUP_FILE}"

# Delete backups older than 30 days
find "${BACKUP_DIR}" -name "icl_*.db.gz" -mtime +30 -delete

echo "Backup completed: ${BACKUP_FILE}.gz"
Make executable and schedule:
chmod +x scripts/backup-db.sh

# Add to crontab (daily at 2 AM)
crontab -e
0 2 * * * /opt/iclapp/scripts/backup-db.sh >> /var/log/iclapp-backup.log 2>&1

Monitoring & Logging

Application Logs

Next.js logs to stdout/stderr by default. Capture with PM2 or systemd.

Database Monitoring

# Check database size
du -h data/icl.db

# Check WAL size (should be small, auto-checkpointed)
du -h data/icl.db-wal

# Vacuum database (optimize, run during low traffic)
sqlite3 data/icl.db "VACUUM;"

Health Check Endpoint

Create src/app/api/health/route.ts:
import { NextResponse } from "next/server";
import { db } from "@/db";
import { sql } from "drizzle-orm";

export async function GET() {
  try {
    // Test database connection
    const result = db.select(sql`1 as ok`).get();
    
    return NextResponse.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
      database: result ? "connected" : "disconnected",
    });
  } catch (error) {
    return NextResponse.json(
      { 
        status: "unhealthy", 
        error: String(error) 
      },
      { status: 503 }
    );
  }
}
Monitor with:
curl http://localhost:3000/api/health

Security Hardening

Session Security

  • Change SESSION_SECRET to 32+ character random string
  • Enable SESSION_SECURE=true with HTTPS
  • Set httpOnly: true for cookies (default)
  • Consider shorter session TTL for sensitive data

Database Security

  • Restrict file permissions: chmod 600 data/icl.db
  • Run application as non-root user
  • Enable foreign key constraints (default)
  • Regular backups with encryption

Network Security

  • Always use HTTPS in production
  • Configure firewall (only 80/443 exposed)
  • Use reverse proxy for rate limiting
  • Enable CORS if needed (API only)

Application Security

  • Keep dependencies updated: npm audit
  • Use environment variables for secrets
  • Implement CSRF protection (Next.js default)
  • Regular security audits

Troubleshooting

SQLite in WAL mode should prevent most locks. If issues persist:
# Check for stale locks
lsof data/icl.db

# Kill stale processes
# Stop app, remove WAL files, restart
rm data/icl.db-wal data/icl.db-shm
Monitor with PM2:
pm2 monit

# Restart if memory exceeds threshold
pm2 restart iclapp
Check for:
  • Unclosed database connections
  • Memory-intensive queries
  • Large result sets not paginated
Verify:
  • Cookie is HTTP-only and secure matches protocol
  • Domain matches your deployment URL
  • Session secret is set correctly
  • Browser accepts cookies
Debug:
// Check session in API route
const session = await getSession();
console.log('Session:', session);
better-sqlite3 requires build tools:Ubuntu/Debian:
sudo apt-get install build-essential python3
npm rebuild better-sqlite3
CentOS/RHEL:
sudo yum groupinstall "Development Tools"
npm rebuild better-sqlite3
Alpine Linux (Docker):
RUN apk add --no-cache python3 make g++

Performance Optimization

Database Indexes

Add indexes for common queries:
-- Index quotation queries by date and status
CREATE INDEX idx_quotations_date ON quotations(date);
CREATE INDEX idx_quotations_status ON quotations(status);
CREATE INDEX idx_quotations_user_id ON quotations(user_id);

-- Index client lookups
CREATE INDEX idx_clients_user_id ON clients(user_id);
CREATE INDEX idx_clients_legal_name ON clients(legal_name);

-- Composite index for filtered queries
CREATE INDEX idx_quotations_user_status_date 
  ON quotations(user_id, status, date);

Caching Strategy

Implement Next.js caching for static data:
// Cache location data (rarely changes)
export const revalidate = 3600; // 1 hour

export async function GET() {
  const locations = await db.select().from(locations);
  return NextResponse.json(locations);
}

Connection Pooling

For high-traffic deployments, consider connection pooling:
// src/db/index.ts
import Database from "better-sqlite3";

const pool = [
  new Database(dbPath, { readonly: true }),
  new Database(dbPath, { readonly: true }),
  new Database(dbPath, { readonly: true }),
];

let currentIndex = 0;

export function getReadConnection() {
  currentIndex = (currentIndex + 1) % pool.length;
  return pool[currentIndex];
}

Next Steps

Quickstart

Create your first quotation

User Management

Set up user roles and permissions

API Reference

Explore the REST API

Backup & Recovery

Database backup best practices

Build docs developers (and LLMs) love