···176176177177- Changing license headers, copyright notices, or any legal text (including `LICENSE`).
178178- Modifying release, signing, or deploy workflows: `.github/workflows/publish-*.yaml`, production Dockerfiles (`Dockerfile`, `client/Dockerfile`), `docker-compose.yaml`, or `package.json` `"scripts"` that affect deployment.
179179-- Database migrations — anything added under `db/src/scripts/<service>/` runs against real data. Confirm schema design and rollback story with a maintainer.
179179+- Database migrations — anything added under `db/src/scripts/<service>/` runs against real data. Confirm schema design and rollback story with a maintainer ensure to use CURRENT_USER to support any user on postgres.
180180- Deleting or renaming an existing GraphQL type or field — this breaks cached Apollo client state and any downstream consumer. Additive changes are usually safe; removals need a migration plan.
181181- Rewiring `server/iocContainer` in a way that changes service lifecycles or startup order — cascading effects on tests and boot.
182182- Auth, session, or request middleware (under `server/api.ts`) — security-sensitive; prefer a small, reviewable PR with explicit callouts.
+2
README.md
···182182183183For historical reference, AWS infrastructure code (CDK, Helm charts, Pulumi, CDKTF) that was previously used for production deployments is available on the [`0.1` branch](https://github.com/roostorg/coop/tree/0.1/.devops). That infrastructure code may have drifted from the current application architecture and is no longer maintained, but can serve as a reference for your own deployment.
184184185185+**IMPORTANT** When you run migrations we create a sample org which contains users with default passwords. Make sure you clean up on production environment
186186+185187## Documentation
186188187189The `/docs` folder includes detailed guides on the UI, architecture, key concepts, how signals in Coop work, and how to get started.
+15
db/src/configs/api-server-pg.ts
···66const __dirname = dirname(fileURLToPath(import.meta.url));
77const relativePath = (it: string) => pathJoin(__dirname, it);
8899+// Opt-in TLS for managed Postgres providers that only accept `hostssl`
1010+// connections. `rejectUnauthorized: false` since some providers issue a
1111+// self-signed per-cluster CA we don't ship. Local docker Postgres has no
1212+// TLS, so this stays off by default.
1313+const ssl =
1414+ process.env.API_SERVER_DATABASE_SSL === 'true'
1515+ ? { require: true, rejectUnauthorized: false }
1616+ : undefined;
1717+918export default makePostgresDatabaseConfig({
1019 defaultScriptFormat: 'sql',
1120 scriptsDirectory: relativePath('../scripts/api-server-pg'),
2121+ maintenanceDatabase:
2222+ process.env.API_SERVER_DATABASE_MAINTENANCE_NAME ?? 'postgres',
1223 driverOpts: {
1324 database: process.env.API_SERVER_DATABASE_NAME!,
1425 username: process.env.API_SERVER_DATABASE_USER!,
···1930 dialect: 'postgres',
2031 schema: 'public',
2132 pool: { max: 20 },
3333+ // Sequelize's pg dialect ignores a top-level `ssl` field; TLS must live
3434+ // under `dialectOptions.ssl`. Spread conditionally so the key is omitted
3535+ // entirely when off (exactOptionalPropertyTypes).
3636+ ...(ssl ? { dialectOptions: { ssl } } : {}),
2237 },
2338});
+21-16
db/src/configs/pg-base.ts
···2626 defaultScriptFormat: PostgresSupportedScriptFormats;
2727 scriptsDirectory: string;
2828 driverOpts: Options & { schema: string };
2929+ maintenanceDatabase?: string;
2930}): DatabaseConfig<PostgresSupportedScriptFormats, PostgresContext> {
3031 const { driverOpts, scriptsDirectory, defaultScriptFormat } = opts;
3232+ // DB used for CREATE/DROP DATABASE (can't be the target DB itself).
3333+ // Defaults to `postgres`; some managed providers use a different name
3434+ // (e.g. `defaultdb`).
3535+ const maintenanceDatabase = opts.maintenanceDatabase ?? 'postgres';
31363237 return {
3338 supportedEnvironments: ['staging', 'prod'],
···119124 },
120125121126 async dropDbAndDisconnect() {
122122- // Verify that the user provided valid credentials for the db we're
123123- // trying to drop, and that it exists, by trying to connect to it;
124124- // throw if we can't.
125127 const targetDbconn = new Sequelize(driverOpts);
126128 await targetDbconn.authenticate();
127129 await targetDbconn.close();
128130129129- // Now, connect to the default `postgres` db, rather than the db that we
130130- // want to drop, because, in postgres, the database currently connected
131131- // to cannot be dropped.
132132- const postgresDbConn = new Sequelize({
131131+ const sequelize = new Sequelize({
133132 ...driverOpts,
134134- database: 'postgres',
133133+ database: maintenanceDatabase,
135134 });
136136- const dbName = driverOpts.database;
137137-138138- await postgresDbConn.query(`DROP DATABASE "${dbName}" WITH (FORCE);`);
139139- await postgresDbConn.close();
135135+ await sequelize.query(
136136+ `DROP DATABASE "${driverOpts.database}" WITH (FORCE);`,
137137+ );
138138+ await sequelize.close();
140139 },
141140142141 async prepareDbAndDisconnect() {
143143- const sequelize = new Sequelize({ ...driverOpts, database: 'postgres' });
144144- const databases = await sequelize.query(
145145- `SELECT * FROM pg_database WHERE datname='${driverOpts.database}'`,
142142+ const sequelize = new Sequelize({
143143+ ...driverOpts,
144144+ database: maintenanceDatabase,
145145+ });
146146+ const existing = await sequelize.query(
147147+ `SELECT 1 FROM pg_database WHERE datname = ?`,
148148+ { replacements: [driverOpts.database], type: QueryTypes.SELECT },
146149 );
147147- if ((databases[1] as any).rows.length === 0) {
150150+ if (existing.length === 0) {
151151+ // DDL identifiers can't be bound; driverOpts.database comes from
152152+ // trusted config (env vars), not user input.
148153 await sequelize.query(`CREATE DATABASE "${driverOpts.database}";`);
149154 }
150155 await sequelize.close();
···1111 updated_at timestamp with time zone DEFAULT now() NOT NULL
1212);
13131414-ALTER TABLE signal_auth_service.integration_configs OWNER TO postgres;
1414+ALTER TABLE signal_auth_service.integration_configs OWNER TO CURRENT_USER;
15151616ALTER TABLE ONLY signal_auth_service.integration_configs
1717 ADD CONSTRAINT integration_configs_pkey PRIMARY KEY (org_id, integration_id);
···3737 write: { host: DATABASE_HOST },
3838 },
3939 pool: {
4040- max: 150,
4040+ max: Number(process.env.SEQUELIZE_POOL_MAX ?? 150),
4141 acquire: 15_000,
4242 // This timeout was made crazy long so that queries which take a long time
4343 // to respond don't cause Sequelize to release the connection back to the