diff --git a/.env.example b/.env.example index 35db1dd..b84690d 100644 --- a/.env.example +++ b/.env.example @@ -27,7 +27,7 @@ DB_CONNECTION=sqlite # DB_USERNAME=root # DB_PASSWORD= -SESSION_DRIVER=database +SESSION_DRIVER=file SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ @@ -35,9 +35,9 @@ SESSION_DOMAIN=null BROADCAST_CONNECTION=log FILESYSTEM_DISK=local -QUEUE_CONNECTION=database +QUEUE_CONNECTION=sync -CACHE_STORE=database +CACHE_STORE=file # CACHE_PREFIX= MEMCACHED_HOST=127.0.0.1 @@ -63,3 +63,9 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +INSFORGE_BASE_URL= +INSFORGE_API_KEY= +INSFORGE_DEV_EMAIL= +INSFORGE_DEV_PASSWORD= +APP_TIMEZONE=Asia/Jakarta diff --git a/.gitignore b/.gitignore index c7cf1fa..7333623 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,18 @@ yarn-error.log /.nova /.vscode /.zed + +# InsForge & AI agent skills +.insforge +.agent +.agents +.augment +.claude +.cline +.github/copilot* +.kilocode +.qoder +.qwen +.roo +.trae +.windsurf diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0fa90b2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# AGENTS.md + + +## InsForge backend + +This project uses [InsForge](https://insforge.dev): an all-in-one, open-source Postgres-based backend (BaaS) that gives this app a database, authentication, file storage, edge functions, realtime, an AI model gateway, and payments through one platform. + +- **Project:** **oss-project** (API base `https://7c8jd2pu.ap-southeast.insforge.app`) +- **Skills:** these InsForge skills are installed for supported coding agents. Reach for them before implementing any InsForge feature instead of guessing the API: + - `insforge`: app code with the `@insforge/sdk` client (database CRUD, auth, storage, edge functions, realtime, AI, email, and Stripe payments). + - `insforge-cli`: backend and infrastructure via the `insforge` CLI (projects, SQL, migrations, RLS policies, storage buckets, functions, secrets, payment setup, schedules, deploys). + - `insforge-debug`: diagnosing failures (SDK/HTTP errors, RLS denials, auth and OAuth issues) and running security or performance audits. + - `insforge-integrations`: wiring external auth providers (Clerk, Auth0, WorkOS, Better Auth, etc.) for JWT-based RLS, or the OKX x402 payment facilitator. + - `find-skills`: discovering additional skills on demand. +- **Credentials:** app code reads keys from `.env.local`; the CLI reads `.insforge/project.json`. Never hardcode or commit keys. + +Key patterns: + +- Database inserts take an array: `insert([{ ... }])`. +- Reference users with `auth.users(id)`; use `auth.uid()` in RLS policies. +- For storage uploads, persist both the returned `url` and `key`. + diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..c46b61e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Services\InsForge\InsForgeClient; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -11,7 +13,15 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(InsForgeClient::class, function (Application $app) { + $config = $app->make('config')->get('insforge'); + + return new InsForgeClient( + $config['base_url'], + $config['api_key'], + $config['timeout'], + ); + }); } /** diff --git a/app/Services/InsForge/InsForgeClient.php b/app/Services/InsForge/InsForgeClient.php new file mode 100644 index 0000000..9d9642f --- /dev/null +++ b/app/Services/InsForge/InsForgeClient.php @@ -0,0 +1,135 @@ +baseUrl, $this->apiKey, $this->timeout, $accessToken); + } + + public function accessToken(): ?string + { + return $this->accessToken; + } + + public function login(string $email, string $password): array + { + return $this->request('POST', '/api/auth/sessions', ['email' => $email, 'password' => $password])->json(); + } + + public function logout(): array + { + return $this->request('POST', '/api/auth/logout')->json(); + } + + public function currentUser(): array + { + return $this->request('GET', '/api/auth/profiles/current')->json(); + } + + public function listRecords(string $table, array $params = []): array + { + return $this->request('GET', "/api/database/records/{$table}", query: $params)->json() ?? []; + } + + public function getRecord(string $table, string $id, array $params = []): ?array + { + $params['id'] = 'eq.' . $id; + $params['limit'] = 1; + + return $this->listRecords($table, $params)[0] ?? null; + } + + public function insertRecord(string $table, array $data, bool $asUser = true): array + { + return $this->request('POST', "/api/database/records/{$table}", $data, asUser: $asUser)->json(); + } + + public function updateRecords(string $table, array $filters, array $data, bool $asUser = true): array + { + $query = $this->buildFilters($filters); + + return $this->request('PATCH', "/api/database/records/{$table}", $data, query: $query, asUser: $asUser)->json(); + } + + public function deleteRecords(string $table, array $filters, bool $asUser = true): array + { + return $this->request('DELETE', "/api/database/records/{$table}", query: $this->buildFilters($filters), asUser: $asUser)->json(); + } + + public function countRecords(string $table, array $filters = []): int + { + $query = $this->buildFilters($filters); + $response = $this->request('HEAD', "/api/database/records/{$table}", query: array_merge($query, ['limit' => 1])); + + return (int) ($response->header('Content-Range') ? explode('/', explode('-', $response->header('Content-Range'))[1])[0] : 0); + } + + public function rpc(string $function, array $args = [], bool $asUser = true): mixed + { + return $this->request('POST', "/api/database/rpc/{$function}", $args, asUser: $asUser)->json(); + } + + public function createUserAsAdmin(array $payload): array + { + return $this->request('POST', '/api/auth/users', $payload)->json(); + } + + public function updateUserAsAdmin(string $userId, array $payload): array + { + return $this->request('PATCH', "/api/auth/users/{$userId}", $payload)->json(); + } + + private function buildFilters(array $filters): array + { + $query = []; + + foreach ($filters as $column => $value) { + $query[$column] = str_starts_with((string) $value, 'eq.') ? $value : 'eq.' . $value; + } + + return $query; + } + + private function request(string $method, string $path, array $body = null, array $query = [], bool $asUser = false): Response + { + if ($asUser && $this->accessToken === null) { + throw new InsForgeException(null, 401, 'No user access token available for this request.'); + } + + try { + $http = Http::baseUrl(rtrim($this->baseUrl, '/')) + ->timeout($this->timeout) + ->withToken($asUser ? $this->accessToken : $this->apiKey); + + $verb = strtolower($method); + + $response = in_array($verb, ['get', 'head']) + ? $http->{$verb}($path, $query) + : $http->{$verb}($path, $body ?? []); + + throw_unless($response->successful(), fn () => InsForgeException::fromResponse( + $response->status(), + $response->json() ?? $response->body(), + )); + + return $response; + } catch (ConnectionException $e) { + throw new InsForgeException(null, 0, 'InsForge connection failed: ' . $e->getMessage()); + } + } +} diff --git a/app/Services/InsForge/InsForgeException.php b/app/Services/InsForge/InsForgeException.php new file mode 100644 index 0000000..a957da5 --- /dev/null +++ b/app/Services/InsForge/InsForgeException.php @@ -0,0 +1,29 @@ + 'UTC', + 'timezone' => env('APP_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- diff --git a/config/insforge.php b/config/insforge.php new file mode 100644 index 0000000..2ca9115 --- /dev/null +++ b/config/insforge.php @@ -0,0 +1,11 @@ + env('INSFORGE_BASE_URL', ''), + + 'api_key' => env('INSFORGE_API_KEY', ''), + + 'timeout' => env('INSFORGE_TIMEOUT', 30), + +];