1020 lines
45 KiB
YAML
1020 lines
45 KiB
YAML
# yaml-language-server: $schema=https://schema.zeabur.app/template.json
|
||
apiVersion: zeabur.com/v1
|
||
kind: Template
|
||
metadata:
|
||
name: InsForge
|
||
spec:
|
||
description: InsForge is the Agent-Native Supabase Alternative, enabling AI agents to build and manage full-stack applications autonomously.
|
||
coverImage: https://cdn.zeabur.com/insforge.png
|
||
icon: https://avatars.githubusercontent.com/u/198419463?s=96&v=4
|
||
variables:
|
||
- key: PUBLIC_DOMAIN
|
||
type: DOMAIN
|
||
name: InsForge Domain
|
||
description: The domain for accessing your InsForge application.
|
||
- key: ROOT_ADMIN_USERNAME
|
||
type: STRING
|
||
name: Root Admin Username
|
||
description: Username for the root admin.
|
||
- key: ROOT_ADMIN_PASSWORD
|
||
type: STRING
|
||
name: Root Admin Password
|
||
description: Password for the root admin. Must be at least 8 characters.
|
||
- key: OPENROUTER_API_KEY
|
||
type: STRING
|
||
name: OpenRouter API Key
|
||
description: API key for OpenRouter LLM services (optional).
|
||
- key: STRIPE_TEST_SECRET_KEY
|
||
type: STRING
|
||
name: Stripe Test Secret Key
|
||
description: Stripe test mode secret key for payment catalog sync (optional).
|
||
- key: STRIPE_LIVE_SECRET_KEY
|
||
type: STRING
|
||
name: Stripe Live Secret Key
|
||
description: Stripe live mode secret key for payment catalog sync (optional).
|
||
tags:
|
||
- Development
|
||
- Database
|
||
- API
|
||
- Platform
|
||
readme: |-
|
||
# InsForge
|
||
|
||
InsForge is the Agent-Native Supabase Alternative, enabling AI agents to build and manage full-stack applications autonomously.
|
||
|
||
## Features
|
||
|
||
- **Authentication System** - User management and secure authentication
|
||
- **Database Storage** - Flexible PostgreSQL database with automatic schema management
|
||
- **File Management** - Secure file upload and storage capabilities
|
||
- **PostgREST API** - Automatic REST API generation from database schema
|
||
- **Serverless Functions** - Edge functions for custom business logic
|
||
- **AI Agent Integration** - Connect AI agents like Claude or GPT to manage your backend
|
||
|
||
## Quick Start
|
||
|
||
This template provides **one-click deployment** of the complete InsForge platform:
|
||
|
||
1. **Deploy** - Click deploy and bind a domain
|
||
2. **Login** - Use your admin credentials to access the dashboard
|
||
3. **Connect AI Agent** - Link your AI agent (Claude, GPT, etc.) through the dashboard
|
||
4. **Build Apps** - Use natural language prompts to create applications:
|
||
- "Build a todo app with user authentication"
|
||
- "Create an Instagram clone with image upload"
|
||
- "Build a blog with comments and likes"
|
||
|
||
## Use Cases
|
||
|
||
- **AI-Generated Frontends** - Rapidly create backends for AI-generated frontend projects
|
||
- **Agent-Driven Development** - Let AI agents manage your entire backend infrastructure
|
||
- **Rapid Prototyping** - Build full-stack applications using natural language
|
||
|
||
## Community
|
||
|
||
- [Discord](https://discord.gg/MPxwj5xVvW) - Join our community
|
||
- [GitHub](https://github.com/InsForge/InsForge) - Contribute to the project
|
||
- Email: info@insforge.dev
|
||
|
||
services:
|
||
- name: postgres
|
||
icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/postgresql.svg
|
||
template: PREBUILT_V2
|
||
spec:
|
||
source:
|
||
image: ghcr.io/insforge/postgres:v15.13.4
|
||
command:
|
||
- docker-entrypoint.sh
|
||
- postgres
|
||
- -c
|
||
- config_file=/etc/postgresql/postgresql.conf
|
||
ports:
|
||
- id: database
|
||
port: 5432
|
||
type: TCP
|
||
volumes:
|
||
- id: data
|
||
dir: /var/lib/postgresql/data
|
||
instructions:
|
||
- title: Connection String
|
||
content: postgres://postgres:${POSTGRES_PASSWORD}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}/insforge
|
||
- title: PostgreSQL Connect Command
|
||
content: psql "postgres://postgres:${POSTGRES_PASSWORD}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}/insforge"
|
||
- title: PostgreSQL username
|
||
content: postgres
|
||
- title: PostgresSQL password
|
||
content: ${POSTGRES_PASSWORD}
|
||
- title: PostgresSQL database
|
||
content: insforge
|
||
- title: PostgreSQL host
|
||
content: ${PORT_FORWARDED_HOSTNAME}
|
||
- title: PostgreSQL port
|
||
content: ${DATABASE_PORT_FORWARDED_PORT}
|
||
env:
|
||
PGDATA:
|
||
default: /var/lib/postgresql/data/pgdata
|
||
POSTGRES_CONNECTION_STRING:
|
||
default: postgres://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/insforge
|
||
expose: true
|
||
POSTGRES_DATABASE:
|
||
default: insforge
|
||
expose: true
|
||
POSTGRES_DB:
|
||
default: insforge
|
||
POSTGRES_HOST:
|
||
default: ${CONTAINER_HOSTNAME}
|
||
expose: true
|
||
POSTGRES_PASSWORD:
|
||
default: ${PASSWORD}
|
||
expose: true
|
||
readonly: true
|
||
POSTGRES_PORT:
|
||
default: ${DATABASE_PORT}
|
||
expose: true
|
||
POSTGRES_URI:
|
||
default: ${POSTGRES_CONNECTION_STRING}
|
||
expose: true
|
||
POSTGRES_USER:
|
||
default: postgres
|
||
POSTGRES_USERNAME:
|
||
default: postgres
|
||
expose: true
|
||
JWT_SECRET:
|
||
default: ${PASSWORD}
|
||
expose: true
|
||
readonly: true
|
||
JWT_EXP:
|
||
default: "3600"
|
||
configs:
|
||
- path: /etc/postgresql/postgresql.conf
|
||
template: |
|
||
# PostgreSQL configuration for InsForge
|
||
# Enable logical replication for Logflare
|
||
|
||
# Listen on all interfaces to allow container connections
|
||
listen_addresses = '*'
|
||
|
||
# Set WAL level to logical for Logflare replication
|
||
wal_level = logical
|
||
|
||
# Set max replication slots (needed for logical replication)
|
||
max_replication_slots = 10
|
||
|
||
# Set max WAL senders
|
||
max_wal_senders = 10
|
||
|
||
shared_preload_libraries = 'pg_cron,http,pgcrypto,insforge_pg_utils'
|
||
insforge.policy_grant_role = 'project_admin'
|
||
insforge.policy_grant_tables = 'storage.objects,realtime.channels,realtime.messages,payments.stripe_checkout_sessions,payments.stripe_customer_portal_sessions,payments.razorpay_orders,payments.razorpay_subscriptions'
|
||
insforge.internal_schemas = 'ai,auth,compute,deployments,email,functions,memory,payments,realtime,schedules,storage,system'
|
||
cron.database_name = 'insforge'
|
||
- path: /docker-entrypoint-initdb.d/01-init.sql
|
||
template: |
|
||
-- init.sql
|
||
-- Create role for anonymous user
|
||
CREATE ROLE anon NOLOGIN;
|
||
|
||
-- Create role for authenticator
|
||
CREATE ROLE authenticated NOLOGIN;
|
||
|
||
-- Create project admin role for admin users
|
||
CREATE ROLE project_admin NOLOGIN;
|
||
|
||
GRANT USAGE ON SCHEMA public TO anon;
|
||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon;
|
||
GRANT USAGE ON SCHEMA public TO authenticated;
|
||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;
|
||
GRANT USAGE ON SCHEMA public TO project_admin;
|
||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO project_admin;
|
||
|
||
-- Grant permissions to roles
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO anon, authenticated, project_admin;
|
||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO anon, authenticated, project_admin;
|
||
|
||
-- Create function to automatically create RLS policies for new tables
|
||
CREATE OR REPLACE FUNCTION public.create_default_policies()
|
||
RETURNS event_trigger AS $$
|
||
DECLARE
|
||
obj record;
|
||
table_schema text;
|
||
table_name text;
|
||
has_rls boolean;
|
||
BEGIN
|
||
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
|
||
LOOP
|
||
-- Extract schema and table name from object_identity
|
||
-- Handle quoted identifiers by removing quotes
|
||
SELECT INTO table_schema, table_name
|
||
split_part(obj.object_identity, '.', 1),
|
||
trim(both '"' from split_part(obj.object_identity, '.', 2));
|
||
-- Check if RLS is enabled on the table
|
||
SELECT INTO has_rls
|
||
rowsecurity
|
||
FROM pg_tables
|
||
WHERE schemaname = table_schema
|
||
AND tablename = table_name;
|
||
-- Only create policies if RLS is enabled
|
||
IF has_rls THEN
|
||
-- Create policy for project_admin role only
|
||
-- Users must define their own policies for anon and authenticated roles
|
||
EXECUTE format('CREATE POLICY "project_admin_policy" ON %s FOR ALL TO project_admin USING (true) WITH CHECK (true)', obj.object_identity);
|
||
END IF;
|
||
END LOOP;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
-- Create event trigger to run the function when new tables are created
|
||
CREATE EVENT TRIGGER create_policies_on_table_create
|
||
ON ddl_command_end
|
||
WHEN TAG IN ('CREATE TABLE')
|
||
EXECUTE FUNCTION public.create_default_policies();
|
||
|
||
-- Create function to handle RLS enablement
|
||
CREATE OR REPLACE FUNCTION public.create_policies_after_rls()
|
||
RETURNS event_trigger AS $$
|
||
DECLARE
|
||
obj record;
|
||
table_schema text;
|
||
table_name text;
|
||
BEGIN
|
||
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'ALTER TABLE'
|
||
LOOP
|
||
-- Extract schema and table name
|
||
-- Handle quoted identifiers by removing quotes
|
||
SELECT INTO table_schema, table_name
|
||
split_part(obj.object_identity, '.', 1),
|
||
trim(both '"' from split_part(obj.object_identity, '.', 2));
|
||
-- Check if table has RLS enabled and no policies yet
|
||
IF EXISTS (
|
||
SELECT 1 FROM pg_tables
|
||
WHERE schemaname = table_schema
|
||
AND tablename = table_name
|
||
AND rowsecurity = true
|
||
) AND NOT EXISTS (
|
||
SELECT 1 FROM pg_policies
|
||
WHERE schemaname = table_schema
|
||
AND tablename = table_name
|
||
) THEN
|
||
-- Create policy for project_admin role only
|
||
-- Users must define their own policies for anon and authenticated roles
|
||
EXECUTE format('CREATE POLICY "project_admin_policy" ON %s FOR ALL TO project_admin USING (true) WITH CHECK (true)', obj.object_identity);
|
||
END IF;
|
||
END LOOP;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
-- Create event trigger for ALTER TABLE commands
|
||
CREATE EVENT TRIGGER create_policies_on_rls_enable
|
||
ON ddl_command_end
|
||
WHEN TAG IN ('ALTER TABLE')
|
||
EXECUTE FUNCTION public.create_policies_after_rls();
|
||
- path: /docker-entrypoint-initdb.d/02-jwt.sql
|
||
template: |
|
||
\set jwt_secret `echo "$JWT_SECRET"`
|
||
\set jwt_exp `echo "$JWT_EXP"`
|
||
|
||
ALTER DATABASE insforge SET "app.settings.jwt_secret" TO :'jwt_secret';
|
||
ALTER DATABASE insforge SET "app.settings.jwt_exp" TO :'jwt_exp';
|
||
healthCheck:
|
||
type: TCP
|
||
port: database
|
||
|
||
- name: postgrest
|
||
icon: https://avatars.githubusercontent.com/u/15115011?s=96&v=4
|
||
dependencies:
|
||
- postgres
|
||
template: PREBUILT_V2
|
||
spec:
|
||
source:
|
||
image: postgrest/postgrest:v12.2.12
|
||
ports:
|
||
- id: api
|
||
port: 3000
|
||
type: HTTP
|
||
env:
|
||
PGHOST:
|
||
default: postgres
|
||
PGPORT:
|
||
default: "5432"
|
||
PGDATABASE:
|
||
default: insforge
|
||
PGUSER:
|
||
default: postgres
|
||
PGPASSWORD:
|
||
default: ${POSTGRES_PASSWORD}
|
||
expose: true
|
||
PGRST_OPENAPI_SERVER_PROXY_URI:
|
||
default: http://localhost:3000
|
||
PGRST_DB_SCHEMA:
|
||
default: public
|
||
PGRST_DB_ANON_ROLE:
|
||
default: anon
|
||
PGRST_JWT_SECRET:
|
||
default: ${JWT_SECRET}
|
||
expose: true
|
||
readonly: true
|
||
PGRST_DB_CHANNEL_ENABLED:
|
||
default: "true"
|
||
PGRST_DB_CHANNEL:
|
||
default: pgrst
|
||
healthCheck:
|
||
type: HTTP
|
||
port: api
|
||
http:
|
||
path: /
|
||
|
||
- name: deno
|
||
icon: https://avatars.githubusercontent.com/u/42048915?s=96&v=4
|
||
dependencies:
|
||
- postgres
|
||
- postgrest
|
||
template: PREBUILT_V2
|
||
spec:
|
||
source:
|
||
image: denoland/deno:alpine-2.0.6
|
||
command:
|
||
- sh
|
||
- -c
|
||
- |
|
||
cd /app &&
|
||
echo 'Downloading Deno dependencies...' &&
|
||
deno cache functions/server.ts &&
|
||
echo 'Starting Deno server on port 7133...' &&
|
||
deno run --allow-net --allow-env --allow-read=./functions/worker-template.js --watch functions/server.ts
|
||
ports:
|
||
- id: runtime
|
||
port: 7133
|
||
type: HTTP
|
||
volumes:
|
||
- id: cache
|
||
dir: /deno-dir
|
||
env:
|
||
PORT:
|
||
default: "7133"
|
||
DENO_ENV:
|
||
default: development
|
||
DENO_DIR:
|
||
default: /deno-dir
|
||
POSTGRES_HOST:
|
||
default: postgres
|
||
POSTGRES_PORT:
|
||
default: "5432"
|
||
POSTGRES_DB:
|
||
default: insforge
|
||
POSTGRES_USER:
|
||
default: postgres
|
||
POSTGRES_PASSWORD:
|
||
default: ${PGPASSWORD}
|
||
POSTGREST_BASE_URL:
|
||
default: http://postgrest:3000
|
||
WORKER_TIMEOUT_MS:
|
||
default: "60000"
|
||
ENCRYPTION_KEY:
|
||
default: ${PASSWORD}
|
||
JWT_SECRET:
|
||
default: ${PGRST_JWT_SECRET}
|
||
configs:
|
||
- path: /app/functions/server.ts
|
||
template: |
|
||
import { Client } from 'https://deno.land/x/postgres@v0.17.0/mod.ts';
|
||
import { join, dirname, fromFileUrl } from 'https://deno.land/std@0.224.0/path/mod.ts';
|
||
|
||
/* eslint-disable no-console */
|
||
const port = parseInt(Deno.env.get('PORT') ?? '7133');
|
||
|
||
console.log(`Deno serverless runtime running on port ${port}`);
|
||
|
||
// Configuration
|
||
const WORKER_TIMEOUT_MS = parseInt(Deno.env.get('WORKER_TIMEOUT_MS') ?? '60000');
|
||
|
||
// Worker template code - loaded on first use
|
||
let workerTemplateCode: string | null = null;
|
||
|
||
async function getWorkerTemplateCode(): Promise<string> {
|
||
if (!workerTemplateCode) {
|
||
const currentDir = dirname(fromFileUrl(import.meta.url));
|
||
workerTemplateCode = await Deno.readTextFile(join(currentDir, 'worker-template.js'));
|
||
}
|
||
return workerTemplateCode;
|
||
}
|
||
|
||
// Decrypt function for Deno (compatible with Node.js encryption)
|
||
async function decryptSecret(ciphertext: string, key: string): Promise<string> {
|
||
try {
|
||
const parts = ciphertext.split(':');
|
||
if (parts.length !== 3) {
|
||
throw new Error('Invalid ciphertext format');
|
||
}
|
||
|
||
// Get the encryption key by hashing the JWT secret
|
||
const keyData = new TextEncoder().encode(key);
|
||
const hashBuffer = await crypto.subtle.digest('SHA-256', keyData);
|
||
const cryptoKey = await crypto.subtle.importKey('raw', hashBuffer, { name: 'AES-GCM' }, false, [
|
||
'decrypt',
|
||
]);
|
||
|
||
// Extract IV, auth tag, and encrypted data
|
||
const iv = Uint8Array.from(parts[0].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||
const authTag = Uint8Array.from(parts[1].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||
const encrypted = Uint8Array.from(parts[2].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||
|
||
// Combine encrypted data and auth tag (GCM expects them together)
|
||
const cipherData = new Uint8Array(encrypted.length + authTag.length);
|
||
cipherData.set(encrypted);
|
||
cipherData.set(authTag, encrypted.length);
|
||
|
||
// Decrypt
|
||
const decryptedBuffer = await crypto.subtle.decrypt(
|
||
{ name: 'AES-GCM', iv },
|
||
cryptoKey,
|
||
cipherData
|
||
);
|
||
|
||
return new TextDecoder().decode(decryptedBuffer);
|
||
} catch (error) {
|
||
console.error('Failed to decrypt secret:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Database connection
|
||
const dbConfig = {
|
||
user: Deno.env.get('POSTGRES_USER') || 'postgres',
|
||
password: Deno.env.get('POSTGRES_PASSWORD') || 'postgres',
|
||
database: Deno.env.get('POSTGRES_DB') || 'insforge',
|
||
hostname: Deno.env.get('POSTGRES_HOST') || 'postgres',
|
||
port: parseInt(Deno.env.get('POSTGRES_PORT') || '5432', 10),
|
||
};
|
||
|
||
// Get function code from database
|
||
async function getFunctionCode(slug: string): Promise<string | null> {
|
||
const client = new Client(dbConfig);
|
||
|
||
try {
|
||
await client.connect();
|
||
|
||
const result = await client.queryObject<{ code: string }>`
|
||
SELECT code FROM functions.definitions
|
||
WHERE slug = ${slug} AND status = 'active'
|
||
`;
|
||
|
||
if (!result.rows.length) {
|
||
return null;
|
||
}
|
||
|
||
return result.rows[0].code;
|
||
} catch (error) {
|
||
console.error(`Error fetching function ${slug}:`, error);
|
||
return null;
|
||
} finally {
|
||
await client.end();
|
||
}
|
||
}
|
||
|
||
// Get all secrets from main secrets table and decrypt them
|
||
async function getFunctionSecrets(): Promise<Record<string, string>> {
|
||
const client = new Client(dbConfig);
|
||
|
||
try {
|
||
await client.connect();
|
||
|
||
// Get the encryption key from environment
|
||
const encryptionKey = Deno.env.get('ENCRYPTION_KEY') || Deno.env.get('JWT_SECRET');
|
||
if (!encryptionKey) {
|
||
console.error('No encryption key available for decrypting secrets');
|
||
return {};
|
||
}
|
||
|
||
// Fetch all active secrets from system.secrets table
|
||
const result = await client.queryObject<{
|
||
key: string;
|
||
value_ciphertext: string;
|
||
}>`
|
||
SELECT key, value_ciphertext
|
||
FROM system.secrets
|
||
WHERE is_active = true
|
||
AND (expires_at IS NULL OR expires_at > NOW())
|
||
`;
|
||
|
||
const secrets: Record<string, string> = {};
|
||
|
||
// Decrypt each secret
|
||
for (const row of result.rows) {
|
||
try {
|
||
secrets[row.key] = await decryptSecret(row.value_ciphertext, encryptionKey);
|
||
} catch (error) {
|
||
console.error(`Failed to decrypt secret ${row.key}:`, error);
|
||
// Skip this secret if decryption fails
|
||
}
|
||
}
|
||
|
||
return secrets;
|
||
} catch (error) {
|
||
console.error('Error fetching secrets:', error);
|
||
return {};
|
||
} finally {
|
||
await client.end();
|
||
}
|
||
}
|
||
|
||
// Execute function in isolated worker
|
||
async function executeInWorker(code: string, request: Request): Promise<Response> {
|
||
// Get worker template
|
||
const template = await getWorkerTemplateCode();
|
||
|
||
// Fetch all function secrets
|
||
const secrets = await getFunctionSecrets();
|
||
|
||
// Create blob for worker
|
||
const workerBlob = new Blob([template], { type: 'application/javascript' });
|
||
const workerUrl = URL.createObjectURL(workerBlob);
|
||
|
||
return new Promise(async (resolve) => {
|
||
const worker = new Worker(workerUrl, { type: 'module' });
|
||
|
||
// Set timeout for worker execution
|
||
const timeout = setTimeout(() => {
|
||
worker.terminate();
|
||
URL.revokeObjectURL(workerUrl);
|
||
resolve(
|
||
new Response(JSON.stringify({ error: 'Function timeout' }), {
|
||
status: 504,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
);
|
||
}, WORKER_TIMEOUT_MS);
|
||
|
||
// Handle worker response
|
||
worker.onmessage = (e) => {
|
||
clearTimeout(timeout);
|
||
worker.terminate();
|
||
URL.revokeObjectURL(workerUrl);
|
||
|
||
if (e.data.success) {
|
||
const { response } = e.data;
|
||
// The worker now properly sends null for bodyless responses
|
||
resolve(
|
||
new Response(response.body, {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: response.headers,
|
||
})
|
||
);
|
||
} else {
|
||
resolve(
|
||
new Response(JSON.stringify({ error: e.data.error }), {
|
||
status: e.data.status || 500,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
);
|
||
}
|
||
};
|
||
|
||
// Handle worker errors
|
||
worker.onerror = (error) => {
|
||
clearTimeout(timeout);
|
||
worker.terminate();
|
||
URL.revokeObjectURL(workerUrl);
|
||
console.error('Worker error:', error);
|
||
resolve(
|
||
new Response(JSON.stringify({ error: 'Worker execution error' }), {
|
||
status: 500,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
);
|
||
};
|
||
|
||
// Prepare request data
|
||
const body = request.body ? await request.text() : null;
|
||
const requestData = {
|
||
url: request.url,
|
||
method: request.method,
|
||
headers: Object.fromEntries(request.headers),
|
||
body,
|
||
};
|
||
|
||
// Send message with code, request data, and secrets
|
||
worker.postMessage({ code, requestData, secrets });
|
||
});
|
||
}
|
||
|
||
Deno.serve({ port }, async (req: Request) => {
|
||
const url = new URL(req.url);
|
||
const pathname = url.pathname;
|
||
|
||
// Health check
|
||
if (pathname === '/health') {
|
||
return new Response(
|
||
JSON.stringify({
|
||
status: 'ok',
|
||
runtime: 'deno',
|
||
version: Deno.version.deno,
|
||
typescript: Deno.version.typescript,
|
||
v8: Deno.version.v8,
|
||
}),
|
||
{
|
||
headers: { 'Content-Type': 'application/json' },
|
||
}
|
||
);
|
||
}
|
||
|
||
// Function execution - match ONLY exact slug, no subpaths
|
||
const slugMatch = pathname.match(/^\/([a-zA-Z0-9_-]+)$/);
|
||
if (slugMatch) {
|
||
const slug = slugMatch[1];
|
||
const startTime = Date.now();
|
||
|
||
// Get function code from database
|
||
const code = await getFunctionCode(slug);
|
||
|
||
if (!code) {
|
||
return new Response(JSON.stringify({ error: 'Function not found or not active' }), {
|
||
status: 404,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
|
||
// Execute in worker with original request
|
||
try {
|
||
const response = await executeInWorker(code, req);
|
||
const duration = Date.now() - startTime;
|
||
|
||
// Log completed invocations only
|
||
console.log(
|
||
JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
level: 'info',
|
||
slug,
|
||
method: req.method,
|
||
status: response.status,
|
||
duration: `${duration}ms`,
|
||
})
|
||
);
|
||
|
||
return response;
|
||
} catch (error) {
|
||
const duration = Date.now() - startTime;
|
||
console.error(
|
||
JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
level: 'error',
|
||
slug,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
duration: `${duration}ms`,
|
||
})
|
||
);
|
||
return new Response(JSON.stringify({ error: 'Function execution failed' }), {
|
||
status: 500,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
}
|
||
|
||
// Runtime info
|
||
if (pathname === '/info') {
|
||
return new Response(
|
||
JSON.stringify({
|
||
runtime: 'deno',
|
||
version: Deno.version,
|
||
env: Deno.env.get('DENO_ENV') || 'production',
|
||
database: {
|
||
host: dbConfig.hostname,
|
||
database: dbConfig.database,
|
||
},
|
||
}),
|
||
{
|
||
headers: { 'Content-Type': 'application/json' },
|
||
}
|
||
);
|
||
}
|
||
|
||
// 404
|
||
return new Response('Not Found', { status: 404 });
|
||
});
|
||
- path: /app/functions/deno.json
|
||
template: |
|
||
{
|
||
"compilerOptions": {
|
||
"lib": ["deno.window", "deno.worker"],
|
||
"strict": true
|
||
},
|
||
"lint": {
|
||
"files": {
|
||
"include": ["./**/*.ts", "./**/*.js"]
|
||
},
|
||
"rules": {
|
||
"tags": ["recommended"]
|
||
}
|
||
},
|
||
"fmt": {
|
||
"files": {
|
||
"include": ["./**/*.ts", "./**/*.js"]
|
||
},
|
||
"options": {
|
||
"useTabs": false,
|
||
"lineWidth": 100,
|
||
"indentWidth": 2,
|
||
"singleQuote": true
|
||
}
|
||
}
|
||
}
|
||
- path: /app/functions/worker-template.js
|
||
template: |
|
||
/**
|
||
* Worker Template for Serverless Functions
|
||
*
|
||
* This code runs inside a Web Worker environment created by Deno.
|
||
* Each worker is created fresh for a single request, executes once, and terminates.
|
||
*/
|
||
/* eslint-env worker */
|
||
/* global self, Request, Deno */
|
||
|
||
// Import SDK at worker level - this will be available to all functions
|
||
import { createClient } from 'npm:@insforge/sdk';
|
||
// Import base64 utilities for encoding/decoding
|
||
import { encodeBase64, decodeBase64 } from 'https://deno.land/std@0.224.0/encoding/base64.ts';
|
||
|
||
// Handle the single message with code, request data, and secrets
|
||
self.onmessage = async (e) => {
|
||
const { code, requestData, secrets = {} } = e.data;
|
||
|
||
try {
|
||
/**
|
||
* MOCK DENO OBJECT EXPLANATION:
|
||
*
|
||
* Why we need a mock Deno object:
|
||
* - Edge functions run in isolated Web Workers (sandboxed environments)
|
||
* - Web Workers don't have access to the real Deno global object for security
|
||
* - We need to provide Deno.env functionality so functions can access secrets
|
||
*
|
||
* How it works:
|
||
* 1. The main server (server.ts) fetches all active secrets from the system.secrets table
|
||
* 2. Only active (is_active=true) and non-expired secrets are included
|
||
* 3. Secrets are decrypted and passed to this worker via the 'secrets' object
|
||
* 4. We create a mock Deno object that provides Deno.env.get()
|
||
* 5. When user code calls Deno.env.get('MY_SECRET'), it reads from our secrets object
|
||
*
|
||
* This allows edge functions to use familiar Deno.env syntax while maintaining security
|
||
* Secrets are managed via the /api/secrets endpoint
|
||
*/
|
||
const mockDeno = {
|
||
// Mock the Deno.env API - only get() is needed for reading secrets
|
||
env: {
|
||
get: (key) => secrets[key] || undefined,
|
||
},
|
||
};
|
||
|
||
/**
|
||
* FUNCTION WRAPPING EXPLANATION:
|
||
*
|
||
* Here we create a wrapper function that will execute the user's code.
|
||
* The user's function expects to have access to:
|
||
* - module.exports (to export their function)
|
||
* - createClient (the Insforge SDK)
|
||
* - Deno (for Deno.env.get() etc.)
|
||
* - encodeBase64, decodeBase64 (base64 encoding utilities)
|
||
*
|
||
* We inject our mockDeno as the 'Deno' parameter, so when the user's code
|
||
* calls Deno.env.get('MY_SECRET'), it's actually calling mockDeno.env.get('MY_SECRET')
|
||
*/
|
||
const wrapper = new Function(
|
||
'exports',
|
||
'module',
|
||
'createClient',
|
||
'Deno',
|
||
'encodeBase64',
|
||
'decodeBase64',
|
||
code
|
||
);
|
||
const exports = {};
|
||
const module = { exports };
|
||
|
||
// Execute the wrapper, passing mockDeno as the Deno global and utility functions
|
||
// This makes Deno.env.get(), encodeBase64(), and decodeBase64() available inside the user's function
|
||
wrapper(exports, module, createClient, mockDeno, encodeBase64, decodeBase64);
|
||
|
||
// Get the exported function
|
||
const functionHandler = module.exports || exports.default || exports;
|
||
|
||
if (typeof functionHandler !== 'function') {
|
||
throw new Error(
|
||
'No function exported. Expected: module.exports = async function(req) { ... }'
|
||
);
|
||
}
|
||
|
||
// Create Request object from data
|
||
const request = new Request(requestData.url, {
|
||
method: requestData.method,
|
||
headers: requestData.headers,
|
||
body: requestData.body,
|
||
});
|
||
|
||
// Execute the function
|
||
const response = await functionHandler(request);
|
||
|
||
// Serialize and send response
|
||
// Properly handle responses with no body
|
||
let body = null;
|
||
|
||
// Only read body if response has content
|
||
// Status codes 204, 205, and 304 should not have a body
|
||
if (![204, 205, 304].includes(response.status)) {
|
||
body = await response.text();
|
||
}
|
||
|
||
const responseData = {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: Object.fromEntries(response.headers),
|
||
body: body,
|
||
};
|
||
|
||
self.postMessage({ success: true, response: responseData });
|
||
} catch (error) {
|
||
// Check if the error is actually a Response object (thrown by the function)
|
||
if (error instanceof Response) {
|
||
// Handle error responses the same way
|
||
let body = null;
|
||
|
||
if (![204, 205, 304].includes(error.status)) {
|
||
body = await error.text();
|
||
}
|
||
|
||
const responseData = {
|
||
status: error.status,
|
||
statusText: error.statusText,
|
||
headers: Object.fromEntries(error.headers),
|
||
body: body,
|
||
};
|
||
self.postMessage({ success: true, response: responseData });
|
||
} else {
|
||
// For actual errors, include status if available
|
||
self.postMessage({
|
||
success: false,
|
||
error: error.message || 'Unknown error',
|
||
status: error.status || 500,
|
||
});
|
||
}
|
||
}
|
||
};
|
||
healthCheck:
|
||
type: HTTP
|
||
port: runtime
|
||
http:
|
||
path: /health
|
||
|
||
- name: insforge
|
||
icon: https://avatars.githubusercontent.com/u/198419463?s=96&v=4
|
||
dependencies:
|
||
- postgres
|
||
- postgrest
|
||
- deno
|
||
template: PREBUILT_V2
|
||
spec:
|
||
source:
|
||
image: ghcr.io/insforge/insforge-oss:latest
|
||
ports:
|
||
- id: web
|
||
port: 7130
|
||
type: HTTP
|
||
- id: dev
|
||
port: 7131
|
||
type: HTTP
|
||
env:
|
||
PORT:
|
||
default: "7130"
|
||
PROJECT_ROOT:
|
||
default: /app
|
||
API_BASE_URL:
|
||
default: ${ZEABUR_WEB_URL}
|
||
VITE_API_BASE_URL:
|
||
default: ${ZEABUR_WEB_URL}
|
||
JWT_SECRET:
|
||
default: ${PGRST_JWT_SECRET}
|
||
ROOT_ADMIN_USERNAME:
|
||
default: ${ROOT_ADMIN_USERNAME}
|
||
ROOT_ADMIN_PASSWORD:
|
||
default: ${ROOT_ADMIN_PASSWORD}
|
||
# PostgreSQL connection
|
||
POSTGRES_HOST:
|
||
default: postgres
|
||
POSTGRES_PORT:
|
||
default: "5432"
|
||
POSTGRES_DB:
|
||
default: insforge
|
||
POSTGRES_USER:
|
||
default: postgres
|
||
POSTGRESDB_PASSWORD:
|
||
default: ${PGPASSWORD}
|
||
DATABASE_URL:
|
||
default: postgres://postgres:${PGPASSWORD}@postgres:5432/insforge
|
||
POSTGREST_BASE_URL:
|
||
default: http://postgrest:3000
|
||
# Deno Runtime URL for serverless functions
|
||
DENO_RUNTIME_URL:
|
||
default: http://deno:7133
|
||
# OAuth Configuration (optional)
|
||
GOOGLE_CLIENT_ID:
|
||
default: ""
|
||
GOOGLE_CLIENT_SECRET:
|
||
default: ""
|
||
GOOGLE_REDIRECT_URI:
|
||
default: ${ZEABUR_WEB_URL}/api/auth/v1/callback
|
||
GITHUB_CLIENT_ID:
|
||
default: ""
|
||
GITHUB_CLIENT_SECRET:
|
||
default: ""
|
||
GITHUB_REDIRECT_URI:
|
||
default: ${ZEABUR_WEB_URL}/api/auth/v1/callback
|
||
# Multi-tenant Cloud Configuration
|
||
DEPLOYMENT_ID:
|
||
default: ${ZEABUR_SERVICE_ID}
|
||
PROJECT_ID:
|
||
default: ${ZEABUR_PROJECT_ID}
|
||
APP_KEY:
|
||
default: ""
|
||
ACCESS_API_KEY:
|
||
default: ""
|
||
ACCESS_ANON_KEY:
|
||
default: ""
|
||
OPENROUTER_API_KEY:
|
||
default: ${OPENROUTER_API_KEY}
|
||
STRIPE_TEST_SECRET_KEY:
|
||
default: ${STRIPE_TEST_SECRET_KEY}
|
||
STRIPE_LIVE_SECRET_KEY:
|
||
default: ${STRIPE_LIVE_SECRET_KEY}
|
||
healthCheck:
|
||
type: HTTP
|
||
port: web
|
||
http:
|
||
path: /api/health
|
||
domainKey: PUBLIC_DOMAIN
|
||
|
||
localization:
|
||
zh-CN:
|
||
description: InsForge 是 Agent-Native 的 Supabase 替代方案,让 AI 智能体能够自主构建和管理全栈应用程序。
|
||
readme: |
|
||
# InsForge
|
||
|
||
InsForge 是 Agent-Native 的 Supabase 替代方案,让 AI 智能体能够自主构建和管理全栈应用程序。
|
||
|
||
## 功能特性
|
||
|
||
- **身份验证系统** - 用户管理和安全认证
|
||
- **数据库存储** - 灵活的 PostgreSQL 数据库,支持自动模式管理
|
||
- **文件管理** - 安全的文件上传和存储功能
|
||
- **PostgREST API** - 从数据库模式自动生成 REST API
|
||
- **无服务器函数** - 用于自定义业务逻辑的边缘函数
|
||
- **AI 智能体集成** - 连接 Claude 或 GPT 等 AI 智能体来管理后端
|
||
|
||
## 快速开始
|
||
|
||
此模板提供 InsForge 平台的**一键部署**:
|
||
|
||
1. **部署** - 点击部署并绑定域名
|
||
2. **登录** - 使用管理员凭据访问控制台
|
||
3. **连接 AI 智能体** - 通过控制台连接您的 AI 智能体(Claude、GPT 等)
|
||
4. **构建应用** - 使用自然语言提示创建应用程序:
|
||
- "构建一个带用户认证的待办事项应用"
|
||
- "创建一个带图片上传的 Instagram 克隆"
|
||
- "构建一个带评论和点赞的博客"
|
||
|
||
## 使用场景
|
||
|
||
- **AI 生成的前端** - 为 AI 生成的前端项目快速创建后端
|
||
- **智能体驱动开发** - 让 AI 智能体管理您的整个后端基础设施
|
||
- **快速原型设计** - 使用自然语言构建全栈应用程序
|
||
|
||
zh-TW:
|
||
description: InsForge 是 Agent-Native 的 Supabase 替代方案,讓 AI 智慧體能夠自主建構和管理全端應用程式。
|
||
readme: |
|
||
# InsForge
|
||
|
||
InsForge 是 Agent-Native 的 Supabase 替代方案,讓 AI 智慧體能夠自主建構和管理全端應用程式。
|
||
|
||
## 功能特色
|
||
|
||
- **身份驗證系統** - 使用者管理和安全認證
|
||
- **資料庫儲存** - 靈活的 PostgreSQL 資料庫,支援自動模式管理
|
||
- **檔案管理** - 安全的檔案上傳和儲存功能
|
||
- **PostgREST API** - 從資料庫模式自動產生 REST API
|
||
- **無伺服器函數** - 用於自訂業務邏輯的邊緣函數
|
||
- **AI 智慧體整合** - 連接 Claude 或 GPT 等 AI 智慧體來管理後端
|
||
|
||
## 快速開始
|
||
|
||
此模版提供 InsForge 平台的**一鍵部署**:
|
||
|
||
1. **部署** - 點擊部署並綁定網域
|
||
2. **登入** - 使用管理員憑證存取控制台
|
||
3. **連接 AI 智慧體** - 透過控制台連接您的 AI 智慧體(Claude、GPT 等)
|
||
4. **建構應用** - 使用自然語言提示建立應用程式:
|
||
- "建構一個帶使用者認證的待辦事項應用"
|
||
- "建立一個帶圖片上傳的 Instagram 複製品"
|
||
- "建構一個帶評論和按讚的部落格"
|
||
|
||
## 使用情境
|
||
|
||
- **AI 產生的前端** - 為 AI 產生的前端專案快速建立後端
|
||
- **智慧體驅動開發** - 讓 AI 智慧體管理您的整個後端基礎設施
|
||
- **快速原型設計** - 使用自然語言建構全端應用程式
|