How does PostgreSQL's citext type affect string comparisons made using LOWER()?
tldr; when comparing case insensitive and sensitive things for equality, you have to be explicit. text is explicitly case-sensitive; citext is explicitly case-insensitive. You should provide a cast for both sides and be explicit

A few things about lower()


lower() is typed
When it's argument are text, it always returns text


A few other points


When you do a comparison with a literal, the type isn't known (it's explicitly unknown internally).
Operators are functions.
Functions coerce the types in PostgreSQL in runtime.


In this case types are as follows, with description

-- text = unknown
-- unknown promoted to text, this has nothing to do with citext
lower(email) = 'user@example.com';

-- text = text
-- this has nothing to do with citext
lower(email) = lower('user@example.com');

-- text = text
-- this has nothing to do with citext    
lower(email) = lower(lower('user@example.com'));

-- citext LIKE text
-- LIKE is smart `operator ~~(citext,text)` via `texticlike`
-- WORKS
email like lower('user@example.com');

-- citext = unknown
-- unknown promoted to citext, there is an `operator =(citext,citext)`
-- WORKS
email = 'user@example.com';

-- citext = text
-- citext promoted to text, there is no `operator =(citext,text)`
-- FAILS
email = lower('user@example.com');


In summary, there is an operator =(citext,citext). So you can

email = lower('user@example.com')::citext;


If you want, or you can define your own operator that sets = to the case insensitive route rather than the case sensitive route. I find that to be horrible practice though, I'll always cast.