feat: phase 0 foundation — InsForge REST client, config, env wiring, dev account
This commit is contained in:
+9
-3
@@ -27,7 +27,7 @@ DB_CONNECTION=sqlite
|
|||||||
# DB_USERNAME=root
|
# DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
# DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=file
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
@@ -35,9 +35,9 @@ SESSION_DOMAIN=null
|
|||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
CACHE_STORE=database
|
CACHE_STORE=file
|
||||||
# CACHE_PREFIX=
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
@@ -63,3 +63,9 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
INSFORGE_BASE_URL=
|
||||||
|
INSFORGE_API_KEY=
|
||||||
|
INSFORGE_DEV_EMAIL=
|
||||||
|
INSFORGE_DEV_PASSWORD=
|
||||||
|
APP_TIMEZONE=Asia/Jakarta
|
||||||
|
|||||||
+15
@@ -21,3 +21,18 @@ yarn-error.log
|
|||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
|
||||||
|
# InsForge & AI agent skills
|
||||||
|
.insforge
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
|
.augment
|
||||||
|
.claude
|
||||||
|
.cline
|
||||||
|
.github/copilot*
|
||||||
|
.kilocode
|
||||||
|
.qoder
|
||||||
|
.qwen
|
||||||
|
.roo
|
||||||
|
.trae
|
||||||
|
.windsurf
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
<!-- INSFORGE:START -->
|
||||||
|
## 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`.
|
||||||
|
<!-- INSFORGE:END -->
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services\InsForge\InsForgeClient;
|
||||||
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -11,7 +13,15 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
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'],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\InsForge;
|
||||||
|
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
use Illuminate\Http\Client\Response;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class InsForgeClient
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $baseUrl,
|
||||||
|
private readonly string $apiKey,
|
||||||
|
private readonly int $timeout = 30,
|
||||||
|
private readonly ?string $accessToken = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withToken(?string $accessToken): self
|
||||||
|
{
|
||||||
|
return new self($this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\InsForge;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class InsForgeException extends RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly ?string $errorCode,
|
||||||
|
public readonly int $statusCode,
|
||||||
|
string $message,
|
||||||
|
) {
|
||||||
|
parent::__construct($message, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromResponse(int $statusCode, array|string $body): self
|
||||||
|
{
|
||||||
|
if (is_array($body)) {
|
||||||
|
return new self(
|
||||||
|
$body['error'] ?? null,
|
||||||
|
$body['statusCode'] ?? $statusCode,
|
||||||
|
$body['message'] ?? 'InsForge request failed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(null, $statusCode, $body !== '' ? $body : "InsForge request failed with status {$statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -65,7 +65,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'base_url' => env('INSFORGE_BASE_URL', ''),
|
||||||
|
|
||||||
|
'api_key' => env('INSFORGE_API_KEY', ''),
|
||||||
|
|
||||||
|
'timeout' => env('INSFORGE_TIMEOUT', 30),
|
||||||
|
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user