Database - civiform/civiform GitHub Wiki

erDiagram
    ACCOUNTS }|--o{ APPLICANTS : ""
    ACCOUNTS ||--o{ TI_ORGANIZATIONS : ""
    ACCOUNTS
    APPLICANTS ||--|{ APPLICATIONS : ""
    PROGRAMS ||--o{ APPLICATIONS : ""
    APPLICATION_STATUSES ||--|{ PROGRAMS : ""
    VERSIONS_PROGRAMS ||--|{ VERSIONS : ""
    VERSIONS_PROGRAMS ||--|{ PROGRAMS : ""
    VERSIONS_QUESTIONS ||--|{ VERSIONS : ""
    VERSIONS_QUESTIONS ||--|{ QUESTIONS : ""
    PROGRAMS_CATEGORIES ||--|{ CATEGORIES : ""
    PROGRAMS_CATEGORIES ||--|{ PROGRAMS : ""
Loading

See this link for information on what Entity Relationship Diagrams mean.

See PostgreSQL documentation for information about column data types. varchar is character varying, timestamp is timestamp without time zone.

public.accounts

AccountModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
email_address varchar. UNIQUE Account email address, either from the login provider or from TI client creation
member_of_group_id bigint FOREIGN KEY To ti_organizations, indicates account is a Trusted Intermediary
managed_by_group_id bigint FOREIGN KEY To ti_organizations, indicates account is a client of a Trusted Intermediary
admin_of varchar Specified programs the account is an administrator of ("Program Admin")
global_admin boolean Global system administrator ("CiviForm Admin")
authority_id varchar UNIQUE ID that uniquely identifies the user of the Account, from login provider
id_tokens jsonb A map of session ids to digitally-signed tokens that convey a profile in an authentication response.
ti_note varchar A note about a client added by a Trusted Intermediary
Indexes
  • accounts_pkey PRIMARY KEY, btree (id)
  • idx_admin_of gin (admin_of)

public.api_keys

ApiKeyModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
name varchar not null Human readable name of the ApiKey.
create_time timestamp not null Timestamp of when the the ApiKey was created.
update_time timestamp not null Timestamp of when the the ApiKey was last modified.
created_by varchar not null The authorityId of the account that created the ApiKey.
retired_time timestamp The time when the ApiKey was retired or empty if it is not retired.
retired_by varchar The authority ID of the account that retired the ApiKey or empty if it is not retired.
key_id varchar not null UNIQUE Identifier for the ApiKey. Paired with the password, comprises the credentials for using the ApiKey.
salted_key_secret varchar not null UNIQUE Salted key secret (a.k.a. password) of the ApiKey. Created by signing the password with the API secret using the SHA-HMAC-256 algorithm.
subnet varchar not null An allowlist of IPv4 addresses that are permitted to authenticate with this ApiKey. Specified using CIDR notation: https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing. This attribute is a CSV, with multiple CIDR blocks separated by commas.
expiration timestamp not null Timestamp of when the ApiKey is no longer valid.
call_count bigint not null The number of requests that have been attempted using this API key.
last_call_ip_address varchar The client IPv4 address of the last request to successfully auth with the ApiKey. Empty if the ApiKey has never been used.
grants jsonb not null Permissions granted to this ApiKey by the admin.
Indexes
  • api_keys_pkey PRIMARY KEY, btree (id)
  • api_key_ids UNIQUE, btree (key_id)

public.applicants

ApplicantModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
preferred_locale varchar Applicant selected default locale
object jsonb not null Map of questions to applicant answers. Used to hydrate the ApplicantData object.
account_id bigint FOREIGN KEY To accounts. There is a 1:1 mapping of accounts to applicants.
when_created timestamp Date applicant record was created
first_name varchar Applicant first name. Set when an applicant answers a name question marked as "Primary Applicant Info" or when a TI enters a client's name.
middle_name varchar Applicant middle name. Set when an applicant answers a name question marked as "Primary Applicant Info" or when a TI enters a client's name.
last_name varchar Applicant last name. Set when an applicant answers a name question marked as "Primary Applicant Info" or when a TI enters a client's name.
email_address varchar Applicant email address. Set when an applicant answers an email question marked as "Primary Applicant Info" or when a TI enters a client's email.
country_code varchar Country code for applicant phone number. Set when an applicant answers a phone question marked as "Primary Applicant Info" or when a TI enters a client's phone number.
phone_number varchar Applicant phone number. Set when an applicant answers a phone question marked as "Primary Applicant Info" or when a TI enters a client's phone number.
date_of_birth date Applicant date of birth. Set when an applicant answers a date question marked as "Primary Applicant Info" or when a TI enters a client's date of birth.
Indexes
  • applicants_pkey PRIMARY KEY, btree (id)
  • index_date_of_birth btree (date_of_birth)
  • index_email_address gin_trgm_ops (email_address)
  • index_first_name gin_trgm_ops (first_name)
  • index_middle_name gin_trgm_ops (middle_name)
  • index_last_name gin_trgm_ops (last_name)
  • index_phone_number gin_trgm_ops (phone_number)
  • index_country_code gin (country_code)
Sample JSON of the object column

For more information view the backend data model page

{
  "applicant": {
    "Address_mailing": {
      "street": "1600 Amphitheatre Pkwy",
      "address line 2": "",
      "city": "Mountain View",
      "state": "CA",
      "zip": "11111"
    },
    "Vehicles_in_household": [
      {
        "entity name": "Daily commuter",
        "license_plate_number": {
          "text": "ABA1056"
        }
      },
      {
        "entity name": "Pickup truck",
        "license_plate_number": {
          "text": "VCS5234"
        }
      }
    ]
  }
}

public.application_events

ApplicationEventModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
application_id bigint FOREIGN KEY To applications. The application the event applies to.
creator_id bigint FOREIGN KEY To accounts. The account that generated the event.
event_type varchar not null While this is a varchar, it ends up being either a 0 or 1 based on this enum, which is also captured in the details.
details jsonb not null Details about the event.
create_time timestamp not null The time the event was created.

This table operates on database triggers for processing application status changes. See this evolution for details on how the triggers work.

Indexes
  • application_events_pkey PRIMARY KEY, btree (id)
  • index_application_events_by_application btree (application_id)
  • index_application_events_by_creator btree (creator_id)
Details JSON blob
{
  "event_type": "STATUS_CHANGE",
  "status_event": {
    "email_sent": true,
    "status_text": "text of the status"
  }
}
{
  "event_type": "NOTE_CHANGE",
  "note_event": {
    "note": "this is a note about the application"
  }
}

public.application_statuses

ApplicationStatusesModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
program_name varchar not null The program name that these statuses apply to. Should be identical to the "name" column for an entry in the programs table.
status_definitions jsonb not null Statuses defined for the program to be applied to applications. Note that this column is being migrated from the application_statuses table in order to decouple statuses from program revisions.
status_definitions_lifecycle_stage varchar not null Whether the statuses defined in this row for the given program are the currently "active" set of statuses, or "obsolete".
create_time timestamp Date when this version of the statuses was created
Indexes
  • application_statuses_pkey PRIMARY KEY, btree (id)
Sample JSON of the status_definitions column
{
  "statuses": [
    {
      "status": "Pending",
      "defaultStatus": true,
      "status_localized": {
        "isRequired": true,
        "translations": {
          "en_US": "Pending",
          "es_US": "Pendiente"
        }
      },
      "email_body_localized": {
        "isRequired": true,
        "translations": {
          "en_US": "This application is pending",
          "es_US": "Esta solicitud está pendiente"
        }
      }
    }
  ]
}

public.applications

ApplicationModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
applicant_id bigint FOREIGN KEY To applicants table
program_id bigint FOREIGN KEY To programs table
object jsonb not null A copy of the object from the applicants table for the applicant at the time of application submission, containing answers to all questions the applicant has answered.
lifecycle_stage varchar Status of the application (draft or active)
submit_time timestamp Date application record was submitted
submitter_email varchar Email of the TI who submitted the application. This is null if the applicant submitted themselves.
create_time timestamp Date application record was created
preferred_locale varchar Applicant selected default locale
latest_status varchar The status currently applied to the application. This column is updated by database triggers from the application_events table.
latest_note varchar The note currently added to the application. This column is updated using database update statements at the ApplicationEventRepository.
submission_duration interval Calculated as (submit_time - create_time). See this
is_admin boolean Indicates if the application belongs to an admin, used as part of the program preview features. See this for more details.
Indexes
  • applications_pkey PRIMARY KEY, btree (id)
  • index_applications_by_submit_time btree (submit_time)
  • index_applications_by_submit_time_and_id btree (submit_time DESC, id DESC)
Sample JSON of the object column

For more information view the backend data model page

{
  "applicant": {
    "Address_mailing": {
      "street": "1600 Amphitheatre Pkwy",
      "address line 2": "",
      "city": "Mountain View",
      "state": "CA",
      "zip": "11111"
    },
    "Vehicles_in_household": [
      {
        "entity name": "Daily commuter",
        "license_plate_number": {
          "text": "ABA1056"
        }
      },
      {
        "entity name": "Pickup truck",
        "license_plate_number": {
          "text": "VCS5234"
        }
      }
    ]
  }
}

public.civiform_setttings

SettingsGroupModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
settings jsonb not null JSON blob with ADMIN_WRITEABLE settings
create_time timestamp not null The date this version of the settings was created
created_by varchar not null What created the settings. Currently always "system", or "test" for unit tests.
Indexes
  • civiform_settings_pkey PRIMARY KEY, btree (id)
  • civiform_settings_create_time UNIQUE, btree (create_time)

public.files

StoredFileModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
name varchar Name of the file as stored in the file bucket.
original_file_name varchar Used primarily for Azure since files are stored as UUIDs. See this
acls jsonb Set which admins for a particular program have access to this file. See this
Indexes
  • files_pkey PRIMARY KEY, btree (id)
  • index_file_names UNIQUE, btree (name)

public.persisted_durable_jobs

PersistedDurableJobModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
job_name varchar not null Name of the job as defined here
execution_time timestamp not null The next scheduled time for the job to run.
success_time timestamp The time when the job run succeeded.
create_time timestamp not null When this scheduled job entry was created.
remaining_attempts smallint not null How many more times we will try rerunning a failed job before giving up.
error_message varchar Error message that was logged when the job failed.
Indexes
  • persisted_durable_jobs_pkey PRIMARY KEY, btree (id)
  • index_persisted_durable_jobs_by_execution_time btree (execution_time)
  • index_persisted_durable_jobs_by_success_time btree (success_time)

public.play_evolutions and public.play_evolutions_lock

Internal tables used by the Play Framework for applying and recording database evolutions. Should not be modified by application code.

public.programs

ProgramModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
name varchar Name of the program, unique per program, but not in the database. Different versions of the same program are linked by their immutable name.
description varchar Private note about the program viewable only to admins.
block_definitions jsonb not null Screens are defined here along with linking questions to screens
slug varchar Used when creating a permalink to the new application of a program. A URL-safe version of the name.
localized_name jsonb Translated versions of the program name
localized_description jsonb Translated versions of the program description
external_link varchar URL to a resource outside of the application
display_mode varchar not null Determines the visibility of the program to different users. See the DisplayMode enum
create_time timestamp Date this program revision was created
last_modified_time timestamp Last time this program was modified (when in draft state)
program_type varchar Denotes if it is a standard program or a common intake form. See ProgramType
eligibility_is_gating boolean If true, eligibility conditions should gate application submission for this program. If false, ineligible applications will still be allowed to be submitted.
localized_confirmation_message jsonb Used to display a custom message on the application confirmation screen after an applicant submits an application
acls jsonb In the Program Details screen, when the program visibility is set to "Visible to selected trusted intermediaries only", this contains the list of IDs of the TI organizations allowed to view this program.
localized_summary_image_description jsonb A localized description of the summary image used for the program, used as alt text on the image.
summary_image_file_key varchar A key used to fetch the program's summary image from cloud storage
localized_short_description jsonb not null Translated versions of a short program description (< 100 characters)
application_steps jsonb not null Overall steps an applicant should be aware of when applying to a program
Indexes
  • programs_pkey PRIMARY KEY, btree (id)
Sample JSON of the block_definitions column
[
  {
    "id": 1,
    "name": "Screen 1",
    "repeaterId": null,
    "description": "Screen 1 Description",
    "hidePredicate": null,
    "optionalPredicate": null,
    "questionDefinitions": [
      {
        "id": 1,
        "optional": false,
        "addressCorrectionEnabled": false
      },
      {
        "id": 2,
        "optional": false,
        "addressCorrectionEnabled": false
      }
    ]
  },
  {
    "id": 2,
    "name": "Screen 2",
    "repeaterId": null,
    "description": "Screen 2 Description",
    "hidePredicate": null,
    "optionalPredicate": null,
    "questionDefinitions": [
      {
        "id": 3,
        "optional": false
      }
    ]
  },
  {
    "id": 3,
    "name": "Screen 3",
    "repeaterId": null,
    "description": "Screen 3 Description",
    "hidePredicate": {
      "action": "SHOW_BLOCK",
      "rootNode": {
        "node": {
          "type": "leaf",
          "value": {
            "type": "LIST_OF_STRINGS",
            "value": "[\"0\"]"
          },
          "scalar": "SELECTION",
          "operator": "IN",
          "questionId": 4
        }
      }
    },
    "optionalPredicate": null,
    "questionDefinitions": [
      {
        "id": 5,
        "optional": false
      }
    ]
  },
  {
    "id": 4,
    "name": "Screen 4",
    "repeaterId": 3,
    "description": "Screen 4 Description",
    "hidePredicate": null,
    "optionalPredicate": null,
    "questionDefinitions": [
      {
        "id": 6,
        "optional": false
      },
      {
        "id": 7,
        "optional": false
      }
    ]
  }
]
Sample JSON of the localized_name, localized_description, localized_confirmation_message, and localized_summary_image_description columns
{
  "isRequired": true,
  "translations": {
    "en_US": "English Text",
    "es_US": "Spanish Text"
  }
}
Sample JSON of the acls column
{
  "tiProgramViewAcls": [1,2]
}

public.questions

QuestionModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
name varchar Immutable name of the question
description varchar Description of the question
question_type varchar Type of question. Corresponds to the QuestionType enum
validation_predicates jsonb Configuration for question validation requirements
enumerator_id bigint The question id of an ENUMERATOR question_type. It signifies that this questions is part of the enumerator and repeats for each entry.
question_options jsonb The list of valid options for questions that have pre-defined answers (e.g. CHECKBOX, DROPDOWN)
question_text jsonb Text of the question presented to the applicant in their preferred language
question_help_text jsonb Help text of the question presented to the applicant in their preferred language
enumerator_entity_type jsonb Text of the thing we are enumerating presented to the applicant in their preferred language
question_tags varchar[] An array of QuestionTags, denoting things such as whether the question answer should be marked as demographic information, or if the question is a Primary Applicant Info question.
create_time timestamp Date this revision of the question was created

For information on versioning questions and programs see the data versioning model page.

Indexes
  • questions_pkey PRIMARY KEY, btree (id)
  • idx_question_tags gin (question_tags)
  • questions_by_name btree (name)
Sample JSON of the validation_predicates column

Question Type: ADDRESS

{
    "type": "address",
    "disallowPoBox": false
}

Question Type: CHECKBOX, DROPDOWN, & RADIO_BUTTON

{
    "type": "multioption",
    "maxChoicesAllowed": 2,
    "minChoicesRequired": 1
}

Question Type: ID & TEXT

{
    "type": "text",
    "maxLength": 9,
    "minLength": 5
}

Question Type: NUMBER

{
    "type": "number",
    "max": 200,
    "min": 1
}
Sample JSON of the question_options column
[
  {
    "id": 0,
    "displayOrder": 0,
    "localizedOptionText": {
      "isRequired": true,
      "translations": {
        "en_US": "English text",
        "es_US": "Spanish text"
      }
    }
  }
]
Sample JSON of the question_text, question_help_text, and enumerator_entity_type columns
{
  "isRequired": true,
  "translations": {
    "en_US": "English text",
    "es_US": "Spanish text"
  }
}

public.ti_organizations

TrustedIntermediaryGroupModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
name varchar not null Name of the organization
description varchar Description of the organization
Indexes
  • ti_organizations_pkey PRIMARY KEY, btree (id)

public.versions

VersionModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
lifecycle_stage varchar not null Defines which versions are active, draft, or obsolete. Corresponds to the LifecycleStage enum
submit_time timestamp Date when last saved
tombstoned_question_names varchar[] When a question is marked as "archived" in a draft, it shows up in this
tombstoned_program_names varchar[] In the future, if we support archiving programs, this will be used. Currently unused.

For information on versioning questions and programs see the data versioning model page.

Indexes
  • versions_pkey PRIMARY KEY, btree (id)

public.versions_programs

Column Type Nullable Properties Description
programs_id bigint not null !!! No actual foreign key reference in db. Foreign key to programs table
versions_id bigint not null !!! No actual foreign key reference in db. Foreign key to versions table
Indexes
  • versions_programs_pkey PRIMARY KEY, btree (id)

public.versions_questions

Column Type Nullable Properties Description
questions_id bigint not null !!! No actual foreign key reference in db. Foreign key to questions table
versions_id bigint not null !!! No actual foreign key reference in db. Foreign key to versions table
Indexes
  • versions_questions_pkey PRIMARY KEY, btree (id)

public.categories

CategoryModel

Column Type Nullable Properties Description
id bigint not null PRIMARY KEY
localized_name jsonb not null Translations of the category name
create_time timestamp not null Timestamp of when the the category was created.
last_modified_time timestamp Timestamp of when the the category was last modified.
lifecycle_stage varchar not null Defines which categories are active or obsolete.
Indexes
  • categories_pkey PRIMARY KEY, btree (id)
Sample JSON of the localized_name column
{
  "isRequired": true,
  "translations": {
    "am": "ኢኮኖሚያዊ",
    "ko": "경제",
    "lo": "ເສດຖະກິດ",
    "so": "Dhaqaale",
    "tl": "Pang-ekonomiya",
    "vi": "Kinh tế",
    "en_US": "Economic",
    "es_US": "Economía",
    "zh_TW": "經濟"
  }
}

public.programs_categories

Column Type Nullable Properties Description
programs_id bigint not null Foreign key to the programs table.
categories_id bigint not null Foreign key to the categories table.
Indexes
  • programs_categories_pkey PRIMARY KEY, btree (programs_id, categories_id)

Writing database evolutions

Be very careful when writing a new database evolution! Once it goes out in a release, if it needs fixes, it's pretty hard to untangle. Evolutions should generally be considered immutable, and should only be changed in the most dire of circumstances, such as it being likely to break new deployments in the future. We should avoid one-way destructive evolutions, and always have a way of reversing the destructive actions in the downs.

Evolutions should really just be for changing the structure of the database and not the data itself. When updating data, a Durable Job is preferred.

Ensure creation/removal of resources is guarded by IF NOT EXISTS and IF EXISTS. This allows manual fixes to happen without preventing the evolution from being applied.

When possible, do not go back and change a previously released evolution. Try to create a new evolution that fixes the problem. If this is not possible, an existing evolution can be changed, but be aware that the Downs section will be run before the new version of the evolution Ups is applied. If this involves destructively removing data, make sure this isn't going to remove critical user data. Also, most production environments will not allow destructive downs by default, and will require turning on the DATABASE_APPLY_DESTRUCTIVE_CHANGES flag temporarily in order to apply the new revision of the evolution.

Durable jobs for data updates

Durable jobs allow updates to data to occur either on startup or as a recurring background job, but we expect data migrations to be done through a startup job. For any data changes via durable jobs, we should thoroughly test the change and include a rollback plan (which would likely involve adding a rollback job to the PR - see example here). We also shouldn't do a migration and delete data in one go to avoid data loss.

SQL Cookbook

These can be executed locally through bin/dev-psql

Question revision lifecycle stages

Shows question revisions and their lifecycle. The distinct usages will hide all other obsolete revisions of which there can be many.

SELECT distinct questions.id AS  question_id, questions.name, versions.lifecycle_stage
FROM questions
LEFT JOIN versions_questions ON (questions.id = versions_questions.questions_id)
LEFT JOIN versions ON versions.id = versions_questions.versions_id 
ORDER BY questions.name, questions.id desc;

Sample output:

question_id |        name         | lifecycle_stage 
-------------+---------------------+-----------------
           2 | Income Verification | draft
           1 | Income Verification | active
           1 | Income Verification | obsolete
           5 | test enum           | draft
           3 | test enum           | active
           3 | test enum           | obsolete

Program revision lifecycle stages

Similar to above but excludes obsolete as there can be many over time.

SELECT DISTINCT programs.id, programs.name, programs.create_time, versions.lifecycle_stage
FROM programs
LEFT JOIN versions_programs ON (programs.id = versions_programs.programs_id)
LEFT JOIN versions on versions.id = versions_programs.versions_id
WHERE lifecycle_stage != 'obsolete'
ORDER BY name, id desc;

Sample output:

 id |     name     |       create_time       | lifecycle_stage 
----+--------------+-------------------------+-----------------
  7 | test program | 2022-04-26 14:25:27.125 | active
  8 | v37 program  | 2022-04-26 14:38:35.596 | draft
  6 | v37 program  | 2022-04-25 15:18:19.592 | active

Full text for question translation

SELECT
 q.name,
 q.description,
 q.question_type,
 q.question_text->'translations'->>'en_US' AS question_text,
 q.question_help_text->'translations'->>'en_US' AS question_help_text,
 q.enumerator_entity_type->'translations'->>'en_US' AS enumerator_entity_type,
 (
    SELECT STRING_AGG(qo->'localizedOptionText'->'translations'->>'en_US', '; ')
    FROM jsonb_array_elements(q.question_options) qo
 ) AS question_options,
 q.last_modified_time
FROM versions v
LEFT JOIN versions_questions vq ON v.id = vq.versions_id
LEFT JOIN questions q ON vq.questions_id = q.id
WHERE v.lifecycle_stage = 'active';

This includes some fields that give information about the question, like the name, description and type, and the other fields are the english text for translation. Repeated fields will be in one cell and separated by a semi-colon. Note: this outputs all the text, so it shouldn't be used to find only text that hasn't been translated.

Full text for program translation

SELECT
 p.name,
 p.description,
 p.display_mode,
 p.last_modified_time,
  (
    SELECT STRING_AGG(qo->'localizedName'->'translations'->>'en_US', '; ')
    FROM jsonb_array_elements(p.block_definitions) qo
 ) AS block_names,
  (
    SELECT STRING_AGG(qo->'localizedDescription'->'translations'->>'en_US', '; ')
    FROM jsonb_array_elements(p.block_definitions) qo
 ) AS block_descriptions,
 p.localized_name->'translations'->>'en_US' AS localized_name,
 p.localized_description->'translations'->>'en_US' AS localized_description,
 p.localized_summary_image_description->'translations'->>'en_US' AS localized_summary_image_description
FROM versions v
LEFT JOIN versions_programs vp ON v.id = vp.versions_id
LEFT JOIN programs p ON vp.programs_id = p.id
WHERE v.lifecycle_stage = 'active';

This includes some fields that give information about the program, like the name, description, display mode and last modified time, and the other fields are the english text for translation. Repeated fields will be in one cell and separated by a semi-colon. Note: this outputs all the text, so it shouldn't be used to find only text that hasn't been translated.

Editing a previously deployed Evolution

Go to the cluster page.

Go to the service page.

Click the Task Definition.

Click container definition.

Under environment variabels...

Add this (DATABASE_APPLY_DESTRUCTIVE_CHANGES: `true):

Back to the cluster page, click update service:

And configure like so, selecting your new task definition and ticking "force new deployment":

Database GUI

We use pgAdmin as a tool to visualize the database and view database logs.
When working locally, run bin/dev-pgadmin and then go to "http://localhost:8012/browser/". The dev DB password is "example".

If you get an error when running bin/dev-pgadmin that /tmp/servers.json: Is a directory, run sudo rmdir /tmp/servers.json

Note: if you're changing any program or question data through pgadmin or psql, you'll have to clear the cache in DevTools to see the change reflected.

⚠️ **GitHub.com Fallback** ⚠️