← Case studies

Case study · Events & social

How JustNetwork powers live event photo galleries with FileAway

JustNetwork lets event organizers run live photo galleries that anyone can contribute to — including photographers and guests who don't have an account. They built that flow on FileAway and fileaway-sdk, and never touch the upload bytes or the storage bucket themselves.

Industry: Events & social networkingStack: Node.js · React · React NativeUse case: external photo intakeJune 3, 2026 · 8 min read
TL;DR —FileAway is the upload/storage backend for JustNetwork's photographer-link gallery flow: people who are not JustNetwork users drop photos into an event via a shareable link, and those photos stream into the live gallery. JustNetwork never handles the upload bytes or the storage bucket.

At a glance

The challenge

A JustNetwork event has a photo gallery. Event managers can upload from inside the app, but the most valuable photos come from the people on the ground — hired photographers and guests who will never sign up for an account. JustNetwork needed a way for those external contributors to add photos to a specific event's gallery without building an upload pipeline, without becoming a middleman for the file bytes, and without handing storage credentials to anyone.

They also wanted the gallery to feel alive: a photo dropped by a photographer should appear in every open browser within seconds, and a dropped webhook should never leave a hole in the gallery.

Two upload paths, one gallery

Photos reach a gallery two ways. Both land as rows in the same EventGallery table and render in the same UI; they differ only in how the display URL is resolved. Only the first uses FileAway.

PathWho uploadsWhere bytes landFileAway?
Photographer linkExternal photographers / guests (no account)FileAway's integration bucket✅ yes
Direct in-app uploadEvent managers via the app UIJustNetwork's own S3 bucket❌ no

Everything below concerns the photographer-link path.

The end-to-end lifecycle

Photographer-link flow
ORGANIZER                JUSTNETWORK BACKEND            FILEAWAY              PHOTOGRAPHER
   │                            │                          │                      │
   │ POST /fileaway-link        │                          │                      │
   ├───────────────────────────▶│ links.create(...)        │                      │
   │                            ├─────────────────────────▶│  mints link          │
   │                            │   {id, slug, uploadUrl}   │                      │
   │  uploadUrl ◀───────────────┤ store on Event           │                      │
   │                            │                          │                      │
   │ ── shares uploadUrl ───────────────────────────────────────────────────────▶│
   │                            │                          │  hosted upload page  │
   │                            │                          │◀── PUT bytes ────────┤
   │                            │  POST /webhook            │  (direct to bucket)  │
   │                            │◀─ upload.created ─────────┤                      │
   │                            │  verify HMAC → 200        │                      │
   │                            │  persist row, dedupe      │                      │
   │                            │  Pusher PHOTO_NEW ──────▶ (browsers update live) │

a · Link creation

When an organizer opens an event's upload settings, JustNetwork's createLinkForEvent calls links.create with the integration id, a label, expiresAt, maxUploads, maxFileSizeBytes, the allowed image types (jpeg / png / heic / webp), uploadMode: 'multiple', requireUploaderInfo: true, branding, a webhook config, and the key that ties it all together — metadata: { eventId }.

createLinkForEvent (essence)
const link = await fileaway.links.create({
  integrationId: process.env.FILEAWAY_INTEGRATION_ID,
  label: "Event " + eventId + " - gallery",
  allowedMimeTypes: ["image/jpeg", "image/png", "image/heic", "image/webp"],
  uploadMode: "multiple",
  requireUploaderInfo: true,
  maxUploads: FILEAWAY_MAX_UPLOADS,
  maxFileSizeBytes: FILEAWAY_MAX_FILE_SIZE,
  expiresAt,
  metadata: { eventId },                 // the correlation key for every webhook
  webhook: { url: SERVER_URL + "/just-network/integrations/fileaway/webhook",
             secret: process.env.FILEAWAY_WEBHOOK_SECRET },
  branding,
});
// store link.id + link.uploadUrl on the event

The returned id and uploadUrl are stored on the event as fileaway_link_id / fileaway_upload_url. Because links.create itself is not idempotent, the controller checks those columns first — so re-requesting the link never mints a duplicate.

b · Upload

The organizer shares uploadUrl. Photographers upload through FileAway's hosted page straight into the bucket. JustNetwork is not in this path at all — no bytes, no credentials, no proxy.

c · Webhook → database

FileAway POSTs upload.createdto JustNetwork's webhook route. The controller reads the X-Webhook-* headers, verifies the HMAC over the raw body with webhooks.verify (which throws on a bad signature or clock skew), acks 200 immediately, then processes asynchronously — writing an EventGallery row, deduping first on webhook_delivery_id, then on fileaway_upload_id, with a unique-constraint catch as a backstop.

The SDK-verified event exposes its kind on .type (not .event), already parsed — so the handler uses the verified object directly, with no JSON.parse and no manual crypto.

d · Realtime

After persisting, broadcastPhotoNew fires a PHOTO_NEW event on the Pusher channel EVENT_GALLERY_{eventId}, carrying an already-resolved display URL and its expiry — so every open browser prepends the new photo instantly.

e · Display

On gallery read, each row is resolved by resolveGalleryUrl. FileAway-origin rows (they have a fileaway_upload_id) go through uploads.getDownloadUrl, which returns a short-lived proxy downloadUrl plus expiresAt — cached in-process per upload, never past expiry. Direct-S3 rows use their stored link with no expiry.

On the web, a RefreshableImage renders the URL as a plain <img> and, just before expiresAt (or on a load error), mints a fresh one — so an expired token never shows a broken image.

f · Backfill / reconcile

Webhooks can be missed — an endpoint down through all retries, a deploy mid-delivery. backfillFromFileaway pages through links.uploads (a Page<Upload>: rows on page.data, walked via page.next()) and upserts anything missing, using the exact same idempotent write as the live path. Upload items come back raw (_id, not the normalized id).

Trust & security boundaries

The SDK in practice

Everything FileAway-facing is funneled through one module — fileawayService.js — using the fileaway-sdk client, with no raw fetch remaining. SDK errors are normalized into a FileawayError (carrying .status / .code) so the controllers' 404-self-heal and status-passthrough keep working. Seven calls cover the whole integration:

SDK callTriggered byFrequency
uploads.getDownloadUrl(id, { expiresIn })Every gallery render (cached)Hot path
webhooks.verify({ payload, signature, timestamp, secret })Each inbound webhookPer upload
links.create(input)Organizer mints a linkRare
links.get(id)Viewing link managementOccasional
links.delete(id)Closing a linkRare
links.update(id, patch)Webhook-URL self-healRare
links.uploads(id, { page, limit })Manual backfill/reconcileRare

Methods they deliberately skip: uploads.download (they never need server-side bytes), links.uploadsAll (backfill uses manual paging to fit the existing dedup loop), and createUploadTicket / completeUploadTicket(uploaders use the hosted page, so brokered in-app uploads aren't needed).

What made it go smoothly

The integration is small and boring in the best way. A handful of FileAway specifics, learned once, kept it clean:

“Our users drop photos into a live gallery from a link, and they land in our own storage — we never run an upload server or touch a byte. The hard parts were already solved.”

— JustNetwork engineering

Build your own upload flow

Connect your S3 or R2 bucket, mint a branded link, and receive files with a few lines of fileaway-sdk.