PostgreSQL job data feed
Job listings in real tables, queryable with plain SQL.
Any PostgreSQL instance works the same way — managed RDS, Cloud SQL, Neon, or a server you run yourself — because Jobo speaks the standard Postgres wire protocol over TLS. It creates the target table on first sync and upserts on job ID, so a re-push updates rows in place instead of appending duplicates, and the incremental run every 15 minutes only touches jobs that actually changed. The table has a fixed relational shape, which is why transformation pipelines are not offered here: use MongoDB or Elasticsearch if you need arbitrary document structures.
- Sync
- Incremental, every 15 min
- Deduplication
- Upsert on job ID
- Pipelines
- Not supported
- Credentials
- AES-256 at rest
Connection configuration
The fields Jobo collects to reach your PostgreSQL instance.
hoststringrequiredDatabase server hostname or IP address.
portnumberrequiredConnection port. Defaults to 5432.
databasestringrequiredTarget database name.
schemastringTarget schema. Defaults to "public".
tablestringTarget table, created automatically. Defaults to "jobs".
usernamestringrequiredDatabase user with write access.
passwordstringrequiredUser password. Encrypted at rest with AES-256.
ssl_modeselectSSL mode: disable, allow, prefer, require, verify-ca, verify-full.
delete_expired_jobsbooleanDelete expired jobs (default). Disable to keep them with an expired_at timestamp.
indexesstringTick the searches you run (keyword, date, location, company, job type, occupation, skills, salary, language) and Jobo indexes the table for them.
{
"host": "db.example.com",
"port": 5432,
"database": "jobs_db",
"schema": "public",
"table": "jobs",
"username": "jobo_sync",
"password": "••••••••",
"ssl_mode": "require",
"delete_expired_jobs": true
}How to sync jobs to PostgreSQL
One-time setup. After this, the feed maintains itself.
- 1
Create a least-privilege role for the feed
Give the feed a role used by nothing else, a password of at least 16 random characters, and a schema of its own — it builds its table there on first sync and owns it, so no other grants are needed. Jobo refuses superusers and weak passwords, and the connection test warns when the role can change data in other schemas.
sqlCREATE ROLE jobo_sync LOGIN PASSWORD '<at-least-16-random-characters>'; GRANT CONNECT ON DATABASE jobs_db TO jobo_sync; CREATE SCHEMA jobo AUTHORIZATION jobo_sync; - 2
Open the network path
Postgres only accepts what you explicitly allow. Confirm listen_addresses covers the interface Jobo reaches, add a pg_hba.conf line for the feed user and Jobo's address, reload the configuration, and open the port in your firewall or cloud security group to that address only. The setup wizard shows the address Jobo connects from.
bash# pg_hba.conf — TLS-only access for the feed role, from Jobo only hostssl jobs_db jobo_sync <jobo-address>/32 scram-sha-256 # apply without a restart sudo -u postgres psql -c 'SELECT pg_reload_conf();' - 3
Point the feed at a dedicated table
Set schema and table to something nothing else writes to, such as jobo.jobs. The schema must already exist — Jobo creates tables, not schemas. Force resync drops and recreates the configured table, so sharing it with application data will lose that data. Keep ssl_mode at require or higher on anything reachable over the internet.
- 4
Activate and query it
The table and its indexes appear on the first push, and every run after that upserts on job ID. With delete_expired_jobs left on, expired listings are removed outright; turn it off and they stay with an expired_at timestamp you can filter on.
sqlSELECT title, company, locations, apply_url FROM public.jobs WHERE expired_at IS NULL ORDER BY date_posted DESC LIMIT 20;
Common issues
What actually goes wrong when connecting PostgreSQL, and the fix.
- high The connection test times out or is refused.
- Work outwards: firewall or security group first, then listen_addresses, then a matching pg_hba.conf line for the feed user and source IP. A refusal usually means Postgres is not listening on that interface; a timeout usually means the packet never arrived.
- high The push fails with "permission denied for schema public".
- The role can connect but cannot create the table. Grant USAGE and CREATE on the target schema. On PostgreSQL 15 and newer the public schema no longer grants CREATE to everyone, so this bites upgraded clusters in particular.
- medium The push fails because the target schema does not exist.
- Automatic creation covers the table only. Run CREATE SCHEMA yourself and grant the feed role USAGE and CREATE on it, or leave the field empty to use public.
- medium Rows the app wrote to the jobs table disappeared.
- Force resync drops and recreates the configured table by design. Give the feed a table of its own and have your application read from it rather than write to it.
- low The occupation column is NULL on rows synced before it appeared.
- A table Jobo created gains a JSONB occupation column on its next sync, added in place without rewriting the table. Rows already there stay NULL until their job is sent again; run a full resync to rewrite every job and fill them all. Jobs Jobo has not classified keep it NULL.
Best practices
- Give the feed its own table — force resync drops and recreates whatever it points at.
- Give the feed a dedicated role and schema, never a superuser, and nothing beyond CONNECT on the database.
- Allow only Jobo's address (shown in the setup wizard) in pg_hba.conf and your firewall, not 0.0.0.0/0.
- Keep ssl_mode at require or higher on any instance reachable over the internet.
- Add your own indexes for the columns you filter and sort on; the feed only creates its own.
- Filter by role with @> on the JSONB occupation column, e.g. occupation @> '{"category": "Healthcare"}' (subcategory and id work the same way), and tick the Occupation index to make it fast — that index cannot serve occupation->>'category' = … comparisons.
- Deliver a saved Feed Builder feed (by location, source, occupation and more) instead of syncing the whole catalogue.
Other destinations
Start syncing jobs to PostgreSQL
Connect the destination, choose all jobs or a saved Feed Builder feed, and let the feed run.