npm package: @nativesquare/upwork
Description: An Upwork component for Convex.
Latest version: 1.0.0

# Upwork Component for Convex

[![npm version](https://badge.fury.io/js/@nativesquare%2Fupwork.svg)](https://badge.fury.io/js/@nativesquare%2Fupwork)

<!-- START: Include on https://convex.dev/components -->

A [Convex component](https://convex.dev/components) that integrates the
[Upwork API](https://developers.upwork.com/) into your Convex app. Search job
postings, fetch individual listings, and manage OAuth authentication — all with
built-in caching that complies with Upwork's API terms.

```ts
const upwork = new Upwork(components.upwork);

// Search jobs
const results = await upwork.searchJobPostings(ctx, {
  marketPlaceJobFilter: { searchExpression_eq: "react" },
  sortAttributes: ["RECENCY"],
});

// Get a single posting (cache-first, fetches from API if not cached)
const posting = await upwork.getJobPosting(ctx, { upworkId: "~01abc123" });
```

Found a bug? Feature request?
[File it here](https://github.com/NativeSquare/upwork/issues).

## Prerequisites

You'll need an Upwork API application. Create one at the
[Upwork Developer Portal](https://www.upwork.com/developer/keys/apply).

## Installation

```sh
npm install @nativesquare/upwork
```

Register the component in your `convex/convex.config.ts`:

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import upwork from "@nativesquare/upwork/convex.config.js";

const app = defineApp();
app.use(upwork);

export default app;
```

## Environment Variables

Set the following environment variables in your Convex deployment:

| Variable               | Description                               |
| ---------------------- | ----------------------------------------- |
| `UPWORK_CLIENT_ID`     | Your Upwork API application client ID     |
| `UPWORK_CLIENT_SECRET` | Your Upwork API application client secret |
| `CONVEX_SITE_URL`      | Your Convex deployment's HTTP Actions URL |

The component reads these automatically — you never need to pass credentials
in your code.

## Setup

### 1. Register HTTP routes

The component needs an HTTP route to handle the OAuth callback from Upwork.
Add this to your `convex/http.ts`:

```ts
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "@nativesquare/upwork";
import { components } from "./_generated/api";

const http = httpRouter();

registerRoutes(http, components.upwork, {
  onSuccess: "http://localhost:5173", // redirect after successful auth
});

export default http;
```

The `onSuccess` option is where the user is redirected after connecting their
Upwork account. If omitted, a plain text success message is shown instead.

### 2. Create the client

```ts
// convex/example.ts
import { action, query } from "./_generated/server";
import { components } from "./_generated/api";
import { Upwork } from "@nativesquare/upwork";

const upwork = new Upwork(components.upwork);
```

## API Reference

### `getAuthorizationUrl()`

Returns the Upwork OAuth authorization URL. Redirect users here to connect
their Upwork account.

```ts
export const getAuthUrl = query({
  args: {},
  handler: async () => {
    return upwork.getAuthorizationUrl();
  },
});
```

### `getAuthStatus(ctx)`

Returns the current OAuth connection status: `"connected"`, `"disconnected"`,
or `"expired"`.

```ts
export const authStatus = query({
  args: {},
  handler: async (ctx) => {
    return await upwork.getAuthStatus(ctx);
  },
});
```

### `searchJobPostings(ctx, opts?)`

Searches the Upwork marketplace and caches the results. Requires an action
context since it makes a live API call.

**Options** (all optional):

- `marketPlaceJobFilter` — filter object with fields such as `searchExpression_eq`, `searchTerm_eq`, `categoryIds_any`, `jobType_eq`, `experienceLevel_eq`, `pagination_eq`, etc. See `MarketPlaceJobFilter` in the package types.
- `sortAttributes` — array of sort fields: `"CLIENT_RATING"`, `"CLIENT_TOTAL_CHARGE"`, `"RECENCY"`, `"RELEVANCE"`. Defaults to `["RECENCY"]` when omitted.

```ts
export const search = action({
  args: { searchQuery: v.optional(v.string()) },
  handler: async (ctx, args) => {
    return await upwork.searchJobPostings(ctx, {
      marketPlaceJobFilter: args.searchQuery
        ? { searchExpression_eq: args.searchQuery }
        : undefined,
      sortAttributes: ["RECENCY"],
    });
  },
});
```

Returns `{ totalCount, postings, hasNextPage }`.

### `getJobPosting(ctx, opts)`

Gets a single job posting by its Upwork ID. Uses a hybrid strategy: checks the
local cache first, and if not found, fetches from the Upwork API and stores the
result. Requires an action context.

```ts
export const getJob = action({
  args: { upworkId: v.string() },
  handler: async (ctx, args) => {
    return await upwork.getJobPosting(ctx, { upworkId: args.upworkId });
  },
});
```

Returns a `JobPosting` or `null` if the posting doesn't exist.

### `listJobPostings(ctx, opts?)`

Lists cached job postings from the database. This is a query (no API call),
so it's reactive and fast.

```ts
export const list = query({
  args: { limit: v.optional(v.number()) },
  handler: async (ctx, args) => {
    return await upwork.listJobPostings(ctx, { limit: args.limit });
  },
});
```

Returns up to `limit` postings (default 50) cached within the last 23 hours.

### `exchangeAuthCode(ctx, opts)`

Exchanges an OAuth authorization code for access tokens. You typically don't
need to call this directly — `registerRoutes` handles it via the callback
endpoint.

## Upwork API Compliance

This component caches job postings from the Upwork API to improve performance
and reduce API calls. It is designed to comply with
[Upwork's API Terms of Use](https://www.upwork.com/legal#api):

- **24-hour caching limit**: Upwork does not allow storing API data for more
  than 24 hours. This component handles this automatically by stamping each
  cached record with a `cachedAt` timestamp and running an hourly cron job that
  purges any records older than 23 hours.

- **Rate limiting**: The Upwork API enforces a rate limit of **300 requests per
  minute per IP address**. Exceeding this limit will result in HTTP 429
  "Too Many Requests" responses. The caching layer helps you stay within these
  limits — cached data is served directly from the database without hitting the
  Upwork API. Be mindful of how frequently you call `searchJobPostings` and
  `getJobPosting`, as they may make live API requests.

- **Token refresh**: Access tokens are automatically refreshed when expired.

<!-- END: Include on https://convex.dev/components -->

## Development

```sh
pnpm i
pnpm run dev
```

See the [example app](./example/convex/example.ts) for a complete working
integration.