File uploads for deployed apps use presigned S3 URLs. The client uploads directly to S3 (no proxy through the server), supporting files up to 30GB with progress tracking.

How It Works

  1. Init - App calls POST /api/<APP_GUID>/uploads/init with file metadata. Server returns a presigned S3 URL.
  2. Upload - Client PUTs the file directly to the presigned URL (S3). No auth header needed - the URL is pre-authorized.
  3. Complete - App calls POST /api/<APP_GUID>/uploads/complete. Server copies the file to permanent storage, creates the VFS record, generates variants (thumbnails for images, text for PDFs).

Client Helper Library

Include the helper script for automatic progress tracking, multipart handling, and retries:

<script src="https://media.gipity.ai/scripts/gipity-upload.js"></script>

Usage:

const result = await Gipity.upload(fileInput.files[0], {
  appGuid: '<APP_GUID>',
  appToken: token,
  onProgress: (pct) => progressBar.style.width = pct + '%',
  public: false,
  table: 'attachments',
  recordId: 'rec_123',
});
console.log(result.guid, result.url);

The helper handles single-part uploads (< 5GB) and multipart uploads (5GB+) transparently. It retries failed parts up to 3 times with exponential backoff.

Endpoints

POST /api//uploads/init

Get a presigned upload URL.

Auth: App token (X-App-Token), API key (X-Api-Key), or session cookie.

Request:

{
  "filename": "photo.jpg",
  "content_type": "image/jpeg",
  "size": 2048576,
  "public": false,
  "table": "incidents",
  "record_id": "42",
  "path": "/uploads"
}

Response (single-part, < 5GB):

{
  "data": {
    "upload_guid": "fl_Xk7mNp2R",
    "method": "PUT",
    "url": "https://muda.gipity.ai.s3...",
    "headers": { "Content-Type": "image/jpeg" },
    "expires_in": 600
  }
}

Response (multipart, >= 5GB):

{
  "data": {
    "upload_guid": "fl_Xk7mNp2R",
    "method": "multipart",
    "upload_id": "abc123...",
    "part_size": 104857600,
    "parts": [
      { "partNumber": 1, "url": "https://..." },
      { "partNumber": 2, "url": "https://..." }
    ],
    "expires_in": 600
  }
}

POST /api//uploads/complete

Finalize the upload after the client has PUT the file to S3.

Request:

{
  "upload_guid": "fl_Xk7mNp2R",
  "parts": [
    { "part_number": 1, "etag": "\"abc123\"" },
    { "part_number": 2, "etag": "\"def456\"" }
  ]
}

Response:

{
  "data": {
    "guid": "fl_Xk7mNp2R",
    "name": "photo.jpg",
    "size": 2048576,
    "content_type": "image/jpeg",
    "width": 1920,
    "height": 1080,
    "url": "https://media.gipity.ai/app-files/...",
    "is_public": false,
    "table": "incidents",
    "record_id": "42",
    "variants": [
      { "type": "thumbnail", "url": "...", "content_type": "image/jpeg", "status": "complete" }
    ]
  }
}

GET /api//files

List uploaded files. Supports filtering.

Query params: table, record_id, path, limit (default 50, max 200), offset

GET /api//files/:guid

Get file metadata + variant list.

GET /api//files/:guid/content

Download file. Redirects to S3 URL (public) or signed URL (private).

GET /api//files/:guid/variants/:type

Get a specific variant (e.g., thumbnail). Redirects to URL.

Variants

Variants are auto-generated on upload for small files (< 10MB):

Limits

Manual Upload (without helper library)

If you can't use the helper library, implement the three-step flow directly:

// 1. Init
const init = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/init`, {
  method: 'POST',
  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filename: file.name,
    content_type: file.type,
    size: file.size,
  }),
}).then(r => r.json());

// 2. Upload to S3
await fetch(init.data.url, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file,
});

// 3. Complete
const result = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/complete`, {
  method: 'POST',
  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({ upload_guid: init.data.upload_guid }),
}).then(r => r.json());

console.log(result.data.url); // permanent file URL

Integration with Records

Files can be associated with records via table and record_id fields (soft reference). Example flow:

  1. User uploads a screenshot with table: 'incidents', record_id: '42'
  2. Frontend lists attachments via GET /api/:app/files?table=incidents&record_id=42
  3. Each attachment shows the thumbnail variant as a preview