@nativesquare/upwork
v1.0.0
Published
An Upwork component for Convex.
Downloads
194
Readme
Upwork Component for Convex
A Convex component that integrates the Upwork API 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.
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.
Prerequisites
You'll need an Upwork API application. Create one at the Upwork Developer Portal.
Installation
npm install @nativesquare/upworkRegister the component in your convex/convex.config.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:
// 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
// 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.
export const getAuthUrl = query({
args: {},
handler: async () => {
return upwork.getAuthorizationUrl();
},
});getAuthStatus(ctx)
Returns the current OAuth connection status: "connected", "disconnected",
or "expired".
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 assearchExpression_eq,searchTerm_eq,categoryIds_any,jobType_eq,experienceLevel_eq,pagination_eq, etc. SeeMarketPlaceJobFilterin the package types.sortAttributes— array of sort fields:"CLIENT_RATING","CLIENT_TOTAL_CHARGE","RECENCY","RELEVANCE". Defaults to["RECENCY"]when omitted.
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.
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.
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:
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
cachedAttimestamp 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
searchJobPostingsandgetJobPosting, as they may make live API requests.Token refresh: Access tokens are automatically refreshed when expired.
Development
pnpm i
pnpm run devSee the example app for a complete working integration.
