DEV Community

Daniel Neata
Daniel Neata

Posted on Originally published at github.com

API Authentication Across Multiple Platforms: A Practical Implementation Guide

API Authentication Across Multiple Platforms: A Practical Implementation Guide

Publishing to several platforms from one GitHub Actions repository sounds simple: store tokens, call APIs, and schedule a job. The difficult part is not sending the request. It is proving that a post was created exactly once, recovering when authentication expires, and keeping credentials out of logs. This guide walks through a small Node.js publisher design suitable for Bluesky, Blogger, and other OAuth-backed APIs. The examples are illustrative; endpoint contracts differ by provider.

Separate credentials by destination

Treat every platform as its own security boundary. A Bluesky app password is not an OAuth refresh token, and a Blogger refresh token cannot be handled as a short-lived access token. Use individual GitHub Actions secrets for each credential. Give each token the minimum scopes needed to create posts. Do not embed tokens in article content, URLs, build artifacts, or debug output.

Use environment variables only at the step that needs them. Avoid printing the complete environment. If an API returns an authentication error, log the HTTP status and a redacted provider error code rather than the raw response body, which can occasionally contain sensitive values.

Build one timeout-aware request helper

Network requests can hang or fail after the remote service has accepted a write. Always set a timeout, distinguish read operations from writes, and do not automatically retry a timed-out create request. Node.js 20+ provides AbortSignal.timeout, which keeps the example dependency-free.


async function apiRequest(url, { method = 'GET', token, body } = {}) {

  const response = await fetch(url, {

    method,

    headers: {

      Authorization: `Bearer ${token}`,

      ...(body ? { 'Content-Type': 'application/json' } : {}),

    },

    body: body ? JSON.stringify(body) : undefined,

    signal: AbortSignal.timeout(15_000),

  });

  if (response.status === 401) throw new Error('AUTH_REQUIRED');

  if (response.status === 429) throw new Error('RATE_LIMITED');

  if (!response.ok) throw new Error(`API_HTTP_${response.status}`);

  return response.json();

}

Enter fullscreen mode Exit fullscreen mode

This helper deliberately avoids logging the request URL because some legacy APIs accept credentials in query parameters. Prefer Authorization headers whenever supported. Rate-limit responses should be handled according to the provider's Retry-After guidance, with a bounded retry policy for safe reads.

Keep authentication lifecycle separate from publication

For OAuth providers, access tokens typically expire before refresh tokens. A refresh operation is a credential-management step, not a reason to retry a post blindly. Refresh securely, then inspect remote publication history before deciding whether a previous attempt needs to be repeated. For app-password systems, create a session for each short-lived job and discard the session afterward.

Record the destination, account identifier, and token expiry where appropriate, but never the token itself. If authentication fails, stop the workflow with a nonzero exit code. A green workflow that silently skipped a broken publisher is a misleading operational signal.

Make the queue claim atomic

Two GitHub scheduled jobs may overlap because cron runs can be delayed. The database should offer an atomic transition from approved to claimed, so that only one worker owns a post. A plain GET followed by POST is not sufficient protection against a race condition. If the queue is empty, an optional curated fallback may run, but only after it confirms the intended account and scans for previously published content.

Design publication as a state machine

Use explicit states: approved, claimed, published, and failed. Save the remote ID and URL after the provider confirms creation. A remote ID is stronger evidence than a successful process exit; a publicly accessible post URL is stronger still. A network timeout during a create request is an uncertain outcome, not proof of failure.


async function publishClaimed(item, provider) {

  const known = await provider.findByFingerprint(item.fingerprint);

  if (known) return { status: 'published', remoteId: known.id };

  try {

    const created = await provider.create(item);

    if (!created?.id) throw new Error('NO_REMOTE_ID');

    return { status: 'published', remoteId: created.id };

  } catch (error) {

    // A write may have succeeded before the connection failed.

    if (error.name === 'TimeoutError' || error.name === 'AbortError') {

      return { status: 'uncertain', reason: 'CHECK_REMOTE_BEFORE_RETRY' };

    }

    throw error;

  }

}

Enter fullscreen mode Exit fullscreen mode

The provider adapter must implement findByFingerprint using the platform's actual read API. This example is a design pattern, not a drop-in implementation: some providers do not support a fingerprint query. In those cases, scan the account's recent posts, store a durable local publication ledger, or use an idempotency key if the API supports one.

Handle pagination and duplicate detection honestly

Fetching the first 100 posts is not a complete history check for a long-lived account. Follow pagination until you have covered the entire relevant window, or stop publishing if you cannot establish that a candidate is unused. A fixed list of ten fallback messages will eventually be exhausted. Treat exhaustion as a normal no-op and generate new editorially reviewed content separately; do not silently cycle the same copy.

Use a stable fingerprint derived from normalized text and destination URL. Preserve the original text for exact comparisons when platforms expose it. A cryptographic hash can identify your own content, but a remote API cannot search by that hash unless you also store a ledger or add an allowed marker.

Configure GitHub Actions to fail visibly

Run syntax checks and dry-run authentication on code changes. Reserve live publication for a scheduled or explicitly authorized job. GitHub cron is best-effort: it can start late, so evaluate publishing windows in the intended timezone and account for daylight saving time. Avoid broad windows that let multiple delayed runs publish the same fallback post without a shared lock.


name: Safe publisher

on:

  workflow_dispatch:

    inputs:

      dry_run:

        type: boolean

        default: true

jobs:

  check:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4

        with:

          node-version: '24'

      - run: node --check publisher.js

      - name: Run publisher

        env:

          PLATFORM_TOKEN: ${{ secrets.PLATFORM_TOKEN }}

          DRY_RUN: ${{ inputs.dry_run }}

        run: node publisher.js

Enter fullscreen mode Exit fullscreen mode

Never interpret a successful syntax check or dry run as a published post. For live runs, emit structured, non-sensitive fields such as platform, slot, claimed item ID, remote post ID, public URL, and final outcome. The monitoring process should flag missing IDs and repeated empty queues without inventing publication success.

Test the failure paths before enabling automation

Test with an invalid token, an empty queue, a 429 response, a delayed cron execution, and a simulated timeout immediately after a write. Also test the case where the remote post exists but the local database update fails. In that case, reconcile the remote ID instead of posting again. Keep dry runs side-effect-free, including avoiding state changes that would consume queued content.

A practical operating checklist

Before enabling a new destination, verify the account identity, permissions, token rotation process, duplicate detection, timeout behavior, scheduled timezone, and evidence logs. Confirm that an error produces a failed job and that a genuinely empty queue produces an explicit no-op. Review published links periodically, because an API response can succeed even if a platform later moderates or removes content.

Conclusion

Reliable multi-platform publishing depends on state, not just schedules. Keep secrets isolated, make queue claims atomic, reconcile uncertain writes, and require remote evidence before calling a publication successful. Those controls are less glamorous than adding another social network, but they are what make unattended automation maintainable.

Source: https://github.com/danielmason77/danielmason-threads-publisher/blob/main/content-library/dev-community/api-authentication-across-multiple-platforms-a-practical-implementation-guide.md

Top comments (0)