Flowforge Logo
4.x
Essentials

Database Schema

Set up your database for drag-and-drop Kanban functionality.

Required Fields

Your model needs these essential fields for Kanban functionality:

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->string('title');                         // Card title
    $table->string('status');                        // Column identifier
    $table->flowforgePositionColumn();               // Drag-and-drop ordering
    $table->timestamps();
});

Enum Support

Flowforge automatically handles PHP BackedEnum for status fields. No manual conversion needed:

// Define your enum
enum TaskStatus: string
{
    case Todo = 'todo';
    case InProgress = 'in_progress';
    case Done = 'done';
}

// Cast in your model
class Task extends Model
{
    protected $casts = [
        'status' => TaskStatus::class,
    ];
}

// Flowforge automatically converts column IDs to enum values
Column::make('todo')      // → TaskStatus::Todo
Column::make('in_progress') // → TaskStatus::InProgress
Column::make('done')      // → TaskStatus::Done
When a card moves between columns, Flowforge detects the BackedEnum cast and converts the column identifier to the appropriate enum value automatically.

Position Column

The flowforgePositionColumn() method creates a DECIMAL(20,10) column for drag-and-drop functionality:

// Default column name 'position'
$table->flowforgePositionColumn();

// Custom column name
$table->flowforgePositionColumn('sort_order');

// Equivalent to:
$table->decimal('position', 20, 10)->nullable();

Position Storage Details

Flowforge v3.x uses DECIMAL(20,10) for position storage:

PropertyValuePurpose
Total Digits20Maximum precision
Decimal Places10Supports ~33 bisections before precision loss
Default Gap65535Initial spacing between positions
Min Gap0.0001Triggers automatic rebalancing

The system uses BCMath for arbitrary precision arithmetic, ensuring consistent calculations across all database systems.

v2.x Note: Previous versions used VARCHAR with binary collations. v3.x uses DECIMAL for mathematical precision and better concurrent handling.

Example Migration

Here's a complete example for adding Flowforge support to an existing table:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('tasks', function (Blueprint $table) {
            $table->flowforgePositionColumn('position');

            // Recommended: Add unique constraint for concurrent safety
            $table->unique(['status', 'position'], 'unique_position_per_column');
        });
    }

    public function down(): void
    {
        Schema::table('tasks', function (Blueprint $table) {
            $table->dropUnique('unique_position_per_column');
            $table->dropColumn('position');
        });
    }
};
Unique Constraint: Adding a unique constraint on [column_field, position] enables Flowforge's retry mechanism for concurrent operations.

Factory Integration

When using factories, ensure position values are generated correctly using the DecimalPosition service:

use Relaticle\Flowforge\Services\DecimalPosition;

class TaskFactory extends Factory
{
    /** @var array<string, string> Track last position per status */
    private static array $lastPositions = [];

    public function definition(): array
    {
        $status = $this->faker->randomElement(['todo', 'in_progress', 'done']);

        return [
            'title' => $this->faker->sentence(3),
            'status' => $status,
            'position' => $this->generatePositionForStatus($status),
        ];
    }

    private function generatePositionForStatus(string $status): string
    {
        if (!isset(self::$lastPositions[$status])) {
            $position = DecimalPosition::forEmptyColumn();
        } else {
            $position = DecimalPosition::after(self::$lastPositions[$status]);
        }

        self::$lastPositions[$status] = $position;

        return $position;
    }
}

DecimalPosition Methods

MethodPurpose
forEmptyColumn()Get initial position for empty column (65535)
after($position)Get position after given position
before($position)Get position before given position
between($after, $before)Get position between two positions (with jitter)
betweenExact($after, $before)Get exact midpoint (deterministic)

Position Management Commands

Flowforge provides three artisan commands for managing positions:

Diagnose Positions

Check for position issues (gaps, inversions, duplicates):

php artisan flowforge:diagnose-positions \
    --model=App\\Models\\Task \
    --column=status \
    --position=position

Rebalance Positions

Redistribute positions evenly when gaps become small:

php artisan flowforge:rebalance-positions \
    --model=App\\Models\\Task \
    --column=status \
    --position=position

Repair Positions

Interactive command to fix corrupted position data:

php artisan flowforge:repair-positions

Strategies available:

  • regenerate - Fresh start for all positions
  • fix_missing - Only fix null positions
  • fix_duplicates - Fix duplicate positions only
  • fix_all - Both missing + duplicates (recommended)

Concurrent Safety

Jitter Mechanism

Each position calculation adds ±5% random jitter, ensuring concurrent users never generate identical positions. This is handled automatically by the DecimalPosition::between() method.

Auto-Rebalancing

When the gap between adjacent cards falls below 0.0001, positions are automatically redistributed with 65535 spacing. This happens transparently during card moves.

Retry Mechanism

If a unique constraint violation occurs (rare with jitter), Flowforge automatically retries:

  • Max attempts: 3
  • Backoff: 50ms, 100ms, 200ms (exponential)
  • Supported databases: SQLite, MySQL, PostgreSQL